MicroFish/backend/app/services/simulation_runner.py

1769 lines
76 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.

"""
OASIS模拟运行器
在后台运行模拟并记录每个Agent的动作支持实时状态监控
"""
import os
import sys
import json
import time
import asyncio
import threading
import subprocess
import signal
import atexit
from typing import Dict, Any, List, Optional, Union
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from queue import Queue
from ..config import Config
from ..utils.logger import get_logger
from .zep_graph_memory_updater import ZepGraphMemoryManager
from .simulation_ipc import SimulationIPCClient, CommandType, IPCResponse
logger = get_logger('mirofish.simulation_runner')
# Cờ đánh dấu đã đăng ký hàm dọn dẹp hay chưa
_cleanup_registered = False
# Kiểm tra hệ điều hành
IS_WINDOWS = sys.platform == 'win32'
class RunnerStatus(str, Enum):
"""Trạng thái của bộ chạy tiến trình mô phỏng"""
IDLE = "idle" # Rảnh rỗi, chưa chạy
STARTING = "starting" # Đang khởi động
RUNNING = "running" # Đang chạy
PAUSED = "paused" # Đã tạm dừng
STOPPING = "stopping" # Đang dừng lại
STOPPED = "stopped" # Đã dừng
COMPLETED = "completed" # Đã hoàn thành
FAILED = "failed" # Bị lỗi
@dataclass
class AgentAction:
"""Bản ghi hành động của Agent"""
round_num: int # Số thứ tự của vòng (round) mô phỏng
timestamp: str # Dấu thời gian
platform: str # Nền tảng thực hiện: twitter / reddit
agent_id: int # ID của agent
agent_name: str # Tên của agent
action_type: str # Loại hành động: CREATE_POST, LIKE_POST, v.v.
action_args: Dict[str, Any] = field(default_factory=dict) # Tham số của hành động
result: Optional[str] = None # Kết quả thực thi
success: bool = True # Hành động có thành công hay không
def to_dict(self) -> Dict[str, Any]:
return {
"round_num": self.round_num,
"timestamp": self.timestamp,
"platform": self.platform,
"agent_id": self.agent_id,
"agent_name": self.agent_name,
"action_type": self.action_type,
"action_args": self.action_args,
"result": self.result,
"success": self.success,
}
@dataclass
class RoundSummary:
"""Tóm tắt thông tin của mỗi vòng (round)"""
round_num: int # Số thứ tự vòng
start_time: str # Thời gian bắt đầu
end_time: Optional[str] = None # Thời gian kết thúc
simulated_hour: int = 0 # Số giờ đã mô phỏng trong vòng này
twitter_actions: int = 0 # Số hành động trên Twitter
reddit_actions: int = 0 # Số hành động trên Reddit
active_agents: List[int] = field(default_factory=list) # Danh sách ID các agent đang hoạt động
actions: List[AgentAction] = field(default_factory=list) # Danh sách các hành động
def to_dict(self) -> Dict[str, Any]:
return {
"round_num": self.round_num,
"start_time": self.start_time,
"end_time": self.end_time,
"simulated_hour": self.simulated_hour,
"twitter_actions": self.twitter_actions,
"reddit_actions": self.reddit_actions,
"active_agents": self.active_agents,
"actions_count": len(self.actions),
"actions": [a.to_dict() for a in self.actions],
}
@dataclass
class SimulationRunState:
"""Trạng thái đang thực thi của tiến trình mô phỏng (cập nhật theo thời gian thực)"""
simulation_id: str
runner_status: RunnerStatus = RunnerStatus.IDLE
# Thông tin tiến độ
current_round: int = 0
total_rounds: int = 0
simulated_hours: int = 0
total_simulation_hours: int = 0
# Các vòng lặp và thời gian độc lập cho từng nền tảng (sử dụng để hiển thị song song hai nền tảng)
twitter_current_round: int = 0
reddit_current_round: int = 0
twitter_simulated_hours: int = 0
reddit_simulated_hours: int = 0
# Trạng thái nền tảng đang chạy
twitter_running: bool = False
reddit_running: bool = False
twitter_actions_count: int = 0
reddit_actions_count: int = 0
# Trạng thái hoàn thành chung của nền tảng (phát hiện qua sự kiện simulation_end trong actions.jsonl)
twitter_completed: bool = False
reddit_completed: bool = False
# Tóm tắt lại ở mỗi vòng
rounds: List[RoundSummary] = field(default_factory=list)
# Các hành động gần nhất (để hiển thị theo thời gian thực (real-time) trên frontend)
recent_actions: List[AgentAction] = field(default_factory=list)
max_recent_actions: int = 50
# Dấu thời gian
started_at: Optional[str] = None
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
completed_at: Optional[str] = None
# Thông tin lỗi
error: Optional[str] = None
# ID tiến trình (PID) (để dừng/hủy tiến trình)
process_pid: Optional[int] = None
def add_action(self, action: AgentAction):
"""Thêm một hành động vào danh sách các hành động gần nhất"""
self.recent_actions.insert(0, action)
if len(self.recent_actions) > self.max_recent_actions:
self.recent_actions = self.recent_actions[:self.max_recent_actions]
if action.platform == "twitter":
self.twitter_actions_count += 1
else:
self.reddit_actions_count += 1
self.updated_at = datetime.now().isoformat()
def to_dict(self) -> Dict[str, Any]:
return {
"simulation_id": self.simulation_id,
"runner_status": self.runner_status.value,
"current_round": self.current_round,
"total_rounds": self.total_rounds,
"simulated_hours": self.simulated_hours,
"total_simulation_hours": self.total_simulation_hours,
"progress_percent": round(self.current_round / max(self.total_rounds, 1) * 100, 1),
# Vòng lặp và thời gian độc lập cho mỗi nền tảng
"twitter_current_round": self.twitter_current_round,
"reddit_current_round": self.reddit_current_round,
"twitter_simulated_hours": self.twitter_simulated_hours,
"reddit_simulated_hours": self.reddit_simulated_hours,
"twitter_running": self.twitter_running,
"reddit_running": self.reddit_running,
"twitter_completed": self.twitter_completed,
"reddit_completed": self.reddit_completed,
"twitter_actions_count": self.twitter_actions_count,
"reddit_actions_count": self.reddit_actions_count,
"total_actions_count": self.twitter_actions_count + self.reddit_actions_count,
"started_at": self.started_at,
"updated_at": self.updated_at,
"completed_at": self.completed_at,
"error": self.error,
"process_pid": self.process_pid,
}
def to_detail_dict(self) -> Dict[str, Any]:
"""Chi tiết thông tin bao gồm các hành động gần nhất"""
result = self.to_dict()
result["recent_actions"] = [a.to_dict() for a in self.recent_actions]
result["rounds_count"] = len(self.rounds)
return result
class SimulationRunner:
"""
Trình chạy mô phỏng
Quy trách nhiệm:
1. Chạy mô phỏng OASIS trong tiến trình nền (background process)
2. Phân tích nhật ký chạy (log), ghi lại hành động của mỗi Agent
3. Cung cấp API truy vấn trạng thái thời gian thực
4. Hỗ trợ thao tác tạm dừng (pause)/dừng (stop)/tiếp tục (resume)
"""
# Thư mục lưu trữ trạng thái chạy
RUN_STATE_DIR = os.path.join(
os.path.dirname(__file__),
'../../uploads/simulations'
)
# Thư mục chứa các script con (script chạy ứng dụng)
SCRIPTS_DIR = os.path.join(
os.path.dirname(__file__),
'../../scripts'
)
# Trạng thái chạy trong bộ nhớ Memory (RAM)
_run_states: Dict[str, SimulationRunState] = {}
_processes: Dict[str, subprocess.Popen] = {}
_action_queues: Dict[str, Queue] = {}
_monitor_threads: Dict[str, threading.Thread] = {}
_stdout_files: Dict[str, Any] = {} # Lưu trữ tay cầm file đầu ra chuẩn (stdout)
_stderr_files: Dict[str, Any] = {} # Lưu trữ tay cầm file lỗi chuẩn (stderr)
# Cấu hình cập nhật bộ nhớ Đồ thị (Graph Memory)
_graph_memory_enabled: Dict[str, bool] = {} # simulation_id -> enabled (Bật/tắt)
@classmethod
def get_run_state(cls, simulation_id: str) -> Optional[SimulationRunState]:
"""Lấy trạng thái chạy hiện tại"""
if simulation_id in cls._run_states:
return cls._run_states[simulation_id]
# Thử tải từ file nếu không có trong memory
state = cls._load_run_state(simulation_id)
if state:
cls._run_states[simulation_id] = state
return state
@classmethod
def _load_run_state(cls, simulation_id: str) -> Optional[SimulationRunState]:
"""Tải trạng thái chạy từ tệp tin (run_state.json)"""
state_file = os.path.join(cls.RUN_STATE_DIR, simulation_id, "run_state.json")
if not os.path.exists(state_file):
return None
try:
with open(state_file, 'r', encoding='utf-8') as f:
data = json.load(f)
state = SimulationRunState(
simulation_id=simulation_id,
runner_status=RunnerStatus(data.get("runner_status", "idle")),
current_round=data.get("current_round", 0),
total_rounds=data.get("total_rounds", 0),
simulated_hours=data.get("simulated_hours", 0),
total_simulation_hours=data.get("total_simulation_hours", 0),
# Các vòng lặp và thời gian độc lập cho mỗi nền tảng
twitter_current_round=data.get("twitter_current_round", 0),
reddit_current_round=data.get("reddit_current_round", 0),
twitter_simulated_hours=data.get("twitter_simulated_hours", 0),
reddit_simulated_hours=data.get("reddit_simulated_hours", 0),
twitter_running=data.get("twitter_running", False),
reddit_running=data.get("reddit_running", False),
twitter_completed=data.get("twitter_completed", False),
reddit_completed=data.get("reddit_completed", False),
twitter_actions_count=data.get("twitter_actions_count", 0),
reddit_actions_count=data.get("reddit_actions_count", 0),
started_at=data.get("started_at"),
updated_at=data.get("updated_at", datetime.now().isoformat()),
completed_at=data.get("completed_at"),
error=data.get("error"),
process_pid=data.get("process_pid"),
)
# Tải danh sách các hành động gần đây
actions_data = data.get("recent_actions", [])
for a in actions_data:
state.recent_actions.append(AgentAction(
round_num=a.get("round_num", 0),
timestamp=a.get("timestamp", ""),
platform=a.get("platform", ""),
agent_id=a.get("agent_id", 0),
agent_name=a.get("agent_name", ""),
action_type=a.get("action_type", ""),
action_args=a.get("action_args", {}),
result=a.get("result"),
success=a.get("success", True),
))
return state
except Exception as e:
logger.error(f"Failed to load run state: {str(e)}")
return None
@classmethod
def _save_run_state(cls, state: SimulationRunState):
"""Lưu trạng thái chạy vào file"""
sim_dir = os.path.join(cls.RUN_STATE_DIR, state.simulation_id)
os.makedirs(sim_dir, exist_ok=True)
state_file = os.path.join(sim_dir, "run_state.json")
data = state.to_detail_dict()
with open(state_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
cls._run_states[state.simulation_id] = state
@classmethod
def start_simulation(
cls,
simulation_id: str,
platform: str = "parallel", # twitter / reddit / parallel
max_rounds: int = None, # Số vòng mô phỏng tối đa (tùy chọn, dùng để cắt ngắn các mô phỏng quá dài)
enable_graph_memory_update: bool = False, # Có liên tục cập nhật hoạt động của Agent vào Zep graph hay không
graph_id: str = None # ID của Zep graph (Bắt buộc nếu bật tính năng cập nhật sơ đồ (graph))
) -> SimulationRunState:
"""
Bắt đầu mô phỏng
Args:
simulation_id: ID mô phỏng
platform: Nền tảng chạy (twitter/reddit/parallel)
max_rounds: Số vòng chạy tối đa (để cắt bớt)
enable_graph_memory_update: Có cập nhật hành vi Agent vào Zep Graph hay không
graph_id: Zep Graph ID
Returns:
SimulationRunState (Trạng thái sau khi cấu hình)
"""
# Kiểm tra xem có tiến trình nào đang chạy không
existing = cls.get_run_state(simulation_id)
if existing and existing.runner_status in [RunnerStatus.RUNNING, RunnerStatus.STARTING]:
raise ValueError(f"Simulation is already running: {simulation_id}")
# Tải cấu hình mô phỏng
sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id)
config_path = os.path.join(sim_dir, "simulation_config.json")
if not os.path.exists(config_path):
raise ValueError(f"Simulation configuration not found, please call the /prepare API first")
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# Khởi tạo trạng thái chạy
time_config = config.get("time_config", {})
total_hours = time_config.get("total_simulation_hours", 72)
minutes_per_round = time_config.get("minutes_per_round", 30)
total_rounds = int(total_hours * 60 / minutes_per_round)
# Nếu chỉ định maximum rounds, tiến hành việc cắt bớt
if max_rounds is not None and max_rounds > 0:
original_rounds = total_rounds
total_rounds = min(total_rounds, max_rounds)
if total_rounds < original_rounds:
logger.info(f"Rounds truncated: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})")
state = SimulationRunState(
simulation_id=simulation_id,
runner_status=RunnerStatus.STARTING,
total_rounds=total_rounds,
total_simulation_hours=total_hours,
started_at=datetime.now().isoformat(),
)
cls._save_run_state(state)
# Nếu tính năng cập nhật bộ nhớ graph được bật, tạo một updater
if enable_graph_memory_update:
if not graph_id:
raise ValueError("graph_id is required to enable graph memory updates")
try:
ZepGraphMemoryManager.create_updater(simulation_id, graph_id)
cls._graph_memory_enabled[simulation_id] = True
logger.info(f"Graph memory update enabled: simulation_id={simulation_id}, graph_id={graph_id}")
except Exception as e:
logger.error(f"Failed to create graph memory updater: {e}")
cls._graph_memory_enabled[simulation_id] = False
else:
cls._graph_memory_enabled[simulation_id] = False
# Xác định script nào sẽ chạy (các script nằm trong thư mục backend/scripts/)
if platform == "twitter":
script_name = "run_twitter_simulation.py"
state.twitter_running = True
elif platform == "reddit":
script_name = "run_reddit_simulation.py"
state.reddit_running = True
else:
script_name = "run_parallel_simulation.py"
state.twitter_running = True
state.reddit_running = True
script_path = os.path.join(cls.SCRIPTS_DIR, script_name)
if not os.path.exists(script_path):
raise ValueError(f"Script no longer exists: {script_path}")
# Tạo hàng đợi các hành động (Queue)
action_queue = Queue()
cls._action_queues[simulation_id] = action_queue
# Bắt đầu chạy tiến trình mô phỏng
try:
# Xây dựng lệnh chạy, sử dụng full path
# Cấu trúc log mới:
# twitter/actions.jsonl - Log cho các hành động trên Twitter
# reddit/actions.jsonl - Log cho các hành động trên Reddit
# simulation.log - Log cho tiến trình chính
cmd = [
sys.executable, # Python Interpreter
script_path,
"--config", config_path, # Use full path to config
]
# Nếu có thiết lập giới hạn vòng tối đa, hãy truyền nó qua dòng lệnh (command line args)
if max_rounds is not None and max_rounds > 0:
cmd.extend(["--max-rounds", str(max_rounds)])
# Tạo tệp log chính để tránh bộ đệm ống dẫn (pipe buffer) stdout/stderr của tiến trình đầy
main_log_path = os.path.join(sim_dir, "simulation.log")
main_log_file = open(main_log_path, 'w', encoding='utf-8')
# Đặt môi trường cho quy trình con để đảm bảo trên Windows được mã hóa thành UTF-8
# Điều này sửa lỗi thư viện của bên thứ 3 khi họ gọi file hệ thống nếu không chỉ định rõ encode.
env = os.environ.copy()
env['PYTHONUTF8'] = '1' # Python 3.7+ hỗ trợ điều này, giúp mọi hàm open() mặc định theo UTF-8
env['PYTHONIOENCODING'] = 'utf-8' # Đảm bảo đầu ra có stdout/stderr dưới dạng UTF-8
# Đặt thư mục làm việc (CWD - Current Working Directory) thành thư mục nơi mô phỏng
# Thiết lập start_new_session=True sẽ tạo ra nhóm các tiến trình con mới, vì thế thông qua os.killpg có thể hủy toàn bộ những cái đó
process = subprocess.Popen(
cmd,
cwd=sim_dir,
stdout=main_log_file,
stderr=subprocess.STDOUT, # Đẩy luồng stderr cũng vào file đó
text=True,
encoding='utf-8', # Explicitly specify encoding
bufsize=1,
env=env, # Đi kèm bộ setting Environment có set UTF-8
start_new_session=True, # Bắt đầu tạo 1 luồng xử lý mới (New process group)
)
# Ghi lại file để cho bước đóng (close) được thực hiện dễ dàng
cls._stdout_files[simulation_id] = main_log_file
cls._stderr_files[simulation_id] = None # Không cần lưu file stderr độc lập nữa
state.process_pid = process.pid
state.runner_status = RunnerStatus.RUNNING
cls._processes[simulation_id] = process
cls._save_run_state(state)
# Khởi động tiểu trình giám sát (Monitor thread)
monitor_thread = threading.Thread(
target=cls._monitor_simulation,
args=(simulation_id,),
daemon=True
)
monitor_thread.start()
cls._monitor_threads[simulation_id] = monitor_thread
logger.info(f"Simulation started successfully: {simulation_id}, pid={process.pid}, platform={platform}")
except Exception as e:
state.runner_status = RunnerStatus.FAILED
state.error = str(e)
cls._save_run_state(state)
raise
return state
@classmethod
def _monitor_simulation(cls, simulation_id: str):
"""Giám sát (Monitor) phân tích nhật ký ghi lại các hành động"""
sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id)
# 新的日志结构:分平台的动作日志
twitter_actions_log = os.path.join(sim_dir, "twitter", "actions.jsonl")
reddit_actions_log = os.path.join(sim_dir, "reddit", "actions.jsonl")
process = cls._processes.get(simulation_id)
state = cls.get_run_state(simulation_id)
if not process or not state:
return
twitter_position = 0
reddit_position = 0
try:
while process.poll() is None: # 进程仍在运行
# 读取 Twitter 动作日志
if os.path.exists(twitter_actions_log):
twitter_position = cls._read_action_log(
twitter_actions_log, twitter_position, state, "twitter"
)
# 读取 Reddit 动作日志
if os.path.exists(reddit_actions_log):
reddit_position = cls._read_action_log(
reddit_actions_log, reddit_position, state, "reddit"
)
# 更新状态
cls._save_run_state(state)
time.sleep(2)
# 进程结束后,最后读取一次日志
if os.path.exists(twitter_actions_log):
cls._read_action_log(twitter_actions_log, twitter_position, state, "twitter")
if os.path.exists(reddit_actions_log):
cls._read_action_log(reddit_actions_log, reddit_position, state, "reddit")
# 进程结束
exit_code = process.returncode
if exit_code == 0:
state.runner_status = RunnerStatus.COMPLETED
state.completed_at = datetime.now().isoformat()
logger.info(f"模拟完成: {simulation_id}")
else:
state.runner_status = RunnerStatus.FAILED
# 从主日志文件读取错误信息
main_log_path = os.path.join(sim_dir, "simulation.log")
error_info = ""
try:
if os.path.exists(main_log_path):
with open(main_log_path, 'r', encoding='utf-8') as f:
error_info = f.read()[-2000:] # 取最后2000字符
except Exception:
pass
state.error = f"进程退出码: {exit_code}, 错误: {error_info}"
logger.error(f"模拟失败: {simulation_id}, error={state.error}")
state.twitter_running = False
state.reddit_running = False
cls._save_run_state(state)
except Exception as e:
logger.error(f"监控线程异常: {simulation_id}, error={str(e)}")
state.runner_status = RunnerStatus.FAILED
state.error = str(e)
cls._save_run_state(state)
finally:
# 停止图谱记忆更新器
if cls._graph_memory_enabled.get(simulation_id, False):
try:
ZepGraphMemoryManager.stop_updater(simulation_id)
logger.info(f"已停止图谱记忆更新: simulation_id={simulation_id}")
except Exception as e:
logger.error(f"停止图谱记忆更新器失败: {e}")
cls._graph_memory_enabled.pop(simulation_id, None)
# 清理进程资源
cls._processes.pop(simulation_id, None)
cls._action_queues.pop(simulation_id, None)
# 关闭日志文件句柄
if simulation_id in cls._stdout_files:
try:
cls._stdout_files[simulation_id].close()
except Exception:
pass
cls._stdout_files.pop(simulation_id, None)
if simulation_id in cls._stderr_files and cls._stderr_files[simulation_id]:
try:
cls._stderr_files[simulation_id].close()
except Exception:
pass
cls._stderr_files.pop(simulation_id, None)
@classmethod
def _read_action_log(
cls,
log_path: str,
position: int,
state: SimulationRunState,
platform: str
) -> int:
"""
Đọc tệp tin nhật ký (log) của hệ thống
Args:
log_path: Đường dẫn tệp nhật ký
position: Vị trí đọc trước đó
state: Đối tượng trạng thái đang chạy
platform: Nền tảng (twitter/reddit)
Returns:
Vị trí đọc mới
"""
# Kiểm tra xem có bật tính năng cập nhật bộ nhớ graph hay không
graph_memory_enabled = cls._graph_memory_enabled.get(state.simulation_id, False)
graph_updater = None
if graph_memory_enabled:
graph_updater = ZepGraphMemoryManager.get_updater(state.simulation_id)
try:
with open(log_path, 'r', encoding='utf-8') as f:
f.seek(position)
for line in f:
line = line.strip()
if line:
try:
action_data = json.loads(line)
# Xử lý các mục của loại sự kiện
if "event_type" in action_data:
event_type = action_data.get("event_type")
# Phát hiện sự kiện simulation_end và đánh dấu nền tảng đã hoàn thành
if event_type == "simulation_end":
if platform == "twitter":
state.twitter_completed = True
state.twitter_running = False
logger.info(f"Twitter simulation completed: {state.simulation_id}, total_rounds={action_data.get('total_rounds')}, total_actions={action_data.get('total_actions')}")
elif platform == "reddit":
state.reddit_completed = True
state.reddit_running = False
logger.info(f"Reddit simulation completed: {state.simulation_id}, total_rounds={action_data.get('total_rounds')}, total_actions={action_data.get('total_actions')}")
# Kiểm tra xem có phải tất cả các nền tảng được bật đều đã hoàn thành hay không
# Nếu chỉ một nền tảng đang chạy, hãy chỉ kiểm tra nền tảng đó
# Nếu 2 nền tảng đang chạy thì yêu cầu phải hoàn thành cả 2 nền tảng
all_completed = cls._check_all_platforms_completed(state)
if all_completed:
state.runner_status = RunnerStatus.COMPLETED
state.completed_at = datetime.now().isoformat()
logger.info(f"Simulation completed for all platforms: {state.simulation_id}")
# Cập nhật thông tin vòng (round_num) (từ sự kiện round_end)
elif event_type == "round_end":
round_num = action_data.get("round", 0)
simulated_hours = action_data.get("simulated_hours", 0)
# Cập nhật thời gian và vòng thứ tự độc lập cho nền tảng
if platform == "twitter":
if round_num > state.twitter_current_round:
state.twitter_current_round = round_num
state.twitter_simulated_hours = simulated_hours
elif platform == "reddit":
if round_num > state.reddit_current_round:
state.reddit_current_round = round_num
state.reddit_simulated_hours = simulated_hours
# Số vòng chung sẽ là số lớn nhất của hai nền tảng
if round_num > state.current_round:
state.current_round = round_num
# Thời gian chung sẽ là số lớn nhất của hai nền tảng
state.simulated_hours = max(state.twitter_simulated_hours, state.reddit_simulated_hours)
continue
action = AgentAction(
round_num=action_data.get("round", 0),
timestamp=action_data.get("timestamp", datetime.now().isoformat()),
platform=platform,
agent_id=action_data.get("agent_id", 0),
agent_name=action_data.get("agent_name", ""),
action_type=action_data.get("action_type", ""),
action_args=action_data.get("action_args", {}),
result=action_data.get("result"),
success=action_data.get("success", True),
)
state.add_action(action)
# Cập nhật thông tin (vòng) round
if action.round_num and action.round_num > state.current_round:
state.current_round = action.round_num
# Nếu cập nhật bộ nhớ graph được bật, thêm hoạt động vào Zep graph
if graph_updater:
graph_updater.add_activity_from_dict(action_data, platform)
except json.JSONDecodeError:
pass
return f.tell()
except Exception as e:
logger.warning(f"Failed to read action logs: {log_path}, error={e}")
return position
@classmethod
def _check_all_platforms_completed(cls, state: SimulationRunState) -> bool:
"""
Kiểm tra xem tất cả các nền tảng có hoàn thành quá trình mô phỏng hay chưa?
Kiểm tra xem nền tảng có được kích hoạt (hay enable) hay không bằng cách xem tệp tin actions.jsonl có tồn tại hay không
Returns:
True Nếu tất cả các nền tảng được bật đều đã hoàn thành
"""
sim_dir = os.path.join(cls.RUN_STATE_DIR, state.simulation_id)
twitter_log = os.path.join(sim_dir, "twitter", "actions.jsonl")
reddit_log = os.path.join(sim_dir, "reddit", "actions.jsonl")
# Kiểm tra xem mình có đang bật nền tảng nào không (Sử dụng cách kiểm tra tệp tin có tồn tại (exist) hay không)
twitter_enabled = os.path.exists(twitter_log)
reddit_enabled = os.path.exists(reddit_log)
# Nền tảng nào chưa xong thì trả về false
if twitter_enabled and not state.twitter_completed:
return False
if reddit_enabled and not state.reddit_completed:
return False
# Phải có ít nhất 1 nền tảng chạy xong thì mới là true. (nếu 1 nền tảng không chạy => False. False and False = False. True and False = True)
return twitter_enabled or reddit_enabled
@classmethod
def _terminate_process(cls, process: subprocess.Popen, simulation_id: str, timeout: int = 10):
"""
Khả năng tương thích nền tảng, dừng một quá trình và các quá trình con (nhánh)
Args:
process: Quy trình để chấm dứt (kill)
simulation_id: Ghi log ID
timeout: Thời gian chờ cho phép tiến trình kết thúc tính bằng giây (seconds)
"""
if IS_WINDOWS:
# Windows: Sử dụng câu lệnh taskkill để xóa cả tiến trình theo cấu trúc branch tree
# /F = Force termination (Xoa bằng mọi giá), /T = Terminate tree (xóa nhánh tiến trình) bao gồm các sub-process
logger.info(f"Đang dừng quá trình (Windows): simulation={simulation_id}, pid={process.pid}")
try:
# Trước hết hãy cố găng dừng mềm
subprocess.run(
['taskkill', '/PID', str(process.pid), '/T'],
capture_output=True,
timeout=5
)
try:
process.wait(timeout=timeout)
except subprocess.TimeoutExpired:
# Nếu không thể dùng dừng mềm (sau timeout), xóa cứng bằng /F
logger.warning(f"Process unresponsive, force terminating: {simulation_id}")
subprocess.run(
['taskkill', '/F', '/PID', str(process.pid), '/T'],
capture_output=True,
timeout=5
)
process.wait(timeout=5)
except Exception as e:
logger.warning(f"taskkill failed, attempting terminate: {e}")
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
else:
# Unix: Sử dụng process group chấm dứt
# Dùng start_new_session=True, giá trị pgid sẽ bằng đúng với PID gốc của tiến trình
pgid = os.getpgid(process.pid)
logger.info(f"Đang dừng nhóm tiến trình (Unix): simulation={simulation_id}, pgid={pgid}")
# Gửi SIGTERM tới toàn bộ process group
os.killpg(pgid, signal.SIGTERM)
try:
process.wait(timeout=timeout)
except subprocess.TimeoutExpired:
# Nếu xảy ra hiện tượng chưa tự hủy sau timeout, xóa cưỡng bức bằng SIGKILL
logger.warning(f"Process group unresponsive to SIGTERM, force terminating: {simulation_id}")
os.killpg(pgid, signal.SIGKILL)
process.wait(timeout=5)
@classmethod
def stop_simulation(cls, simulation_id: str) -> SimulationRunState:
"""Dừng lại tiến trình mô phỏng"""
state = cls.get_run_state(simulation_id)
if not state:
raise ValueError(f"Simulation not found: {simulation_id}")
if state.runner_status not in [RunnerStatus.RUNNING, RunnerStatus.PAUSED]:
raise ValueError(f"Simulation is not running: {simulation_id}, status={state.runner_status}")
state.runner_status = RunnerStatus.STOPPING
cls._save_run_state(state)
# Kết thúc tiến trình con (child process)
process = cls._processes.get(simulation_id)
if process and process.poll() is None:
try:
cls._terminate_process(process, simulation_id)
except ProcessLookupError:
# Quá trình không còn ở đây nữa (Đã thoát hoặc bị đóng)
pass
except Exception as e:
logger.error(f"Failed to terminate process group: {simulation_id}, error={e}")
# Thử thêm một cách nữa để chăc chắn hủy tiến trình
try:
process.terminate()
process.wait(timeout=5)
except Exception:
process.kill()
state.runner_status = RunnerStatus.STOPPED
state.twitter_running = False
state.reddit_running = False
state.completed_at = datetime.now().isoformat()
cls._save_run_state(state)
# Dừng quá trình graph memory updater
if cls._graph_memory_enabled.get(simulation_id, False):
try:
ZepGraphMemoryManager.stop_updater(simulation_id)
logger.info(f"Graph memory update stopped: simulation_id={simulation_id}")
except Exception as e:
logger.error(f"Failed to stop graph memory updater: {e}")
cls._graph_memory_enabled.pop(simulation_id, None)
logger.info(f"Simulation stopped: {simulation_id}")
return state
@classmethod
def _read_actions_from_file(
cls,
file_path: str,
default_platform: Optional[str] = None,
platform_filter: Optional[str] = None,
agent_id: Optional[int] = None,
round_num: Optional[int] = None
) -> List[AgentAction]:
"""
Đọc các hoạt động từ một tệp duy nhất
Args:
file_path: Đường dẫn tệp log của hành động đó
default_platform: Nền tảng mặc định (nếu trong nhật ký không có platform)
platform_filter: Lọc nền tảng (chỉ định platform cần đọc log)
agent_id: Lọc ID của Agent cụ thể
round_num: Lọc số vòng của Agent
"""
if not os.path.exists(file_path):
return []
actions = []
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
try:
data = json.loads(line)
# Bỏ qua các bản ghi không phải hành động (chẳng hạn như là sự kiện về hệ thống: simulation_start, round_start, round_end v.v.)
if "event_type" in data:
continue
# Bỏ lỡ các sự kiện không phải do agent tạo ra (Không có ID đặc trưng của Agent)
if "agent_id" not in data:
continue
# Lấy nền tảng (Platform): Ưu tiên lấy từ bản ghi nếu có `platform`, nếu không thì dùng `default_platform`
record_platform = data.get("platform") or default_platform or ""
# Bộ lọc (Filtering)
if platform_filter and record_platform != platform_filter:
continue
if agent_id is not None and data.get("agent_id") != agent_id:
continue
if round_num is not None and data.get("round") != round_num:
continue
actions.append(AgentAction(
round_num=data.get("round", 0),
timestamp=data.get("timestamp", ""),
platform=record_platform,
agent_id=data.get("agent_id", 0),
agent_name=data.get("agent_name", ""),
action_type=data.get("action_type", ""),
action_args=data.get("action_args", {}),
result=data.get("result"),
success=data.get("success", True),
))
except json.JSONDecodeError:
continue
return actions
@classmethod
def get_all_actions(
cls,
simulation_id: str,
platform: Optional[str] = None,
agent_id: Optional[int] = None,
round_num: Optional[int] = None
) -> List[AgentAction]:
"""
Lấy thông tin tất cả lịch sử hoạt động của các nền tảng (không giới hạn phân trang)
Args:
simulation_id: ID mô phỏng
platform: Bộ lọc nền tảng hoạt động (twitter/reddit)
agent_id: Lọc Agent
round_num: Lọc số vòng
Returns:
Danh sách đầy đủ các actions (sắp xếp theo thời gian mới nhất lên trước)
"""
sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id)
actions = []
# Đọc tệp tin Actions của Twitter (Khai báo tự động điền twitter theo cấu trúc tệp tin log)
twitter_actions_log = os.path.join(sim_dir, "twitter", "actions.jsonl")
if not platform or platform == "twitter":
actions.extend(cls._read_actions_from_file(
twitter_actions_log,
default_platform="twitter", # Điền dữ liệu tự động cho record `platform`
platform_filter=platform,
agent_id=agent_id,
round_num=round_num
))
# Đọc tệp tin Actions của Reddit (Tự động điền phần 'reddit' căn cứ thư mục chứa tệp tin)
reddit_actions_log = os.path.join(sim_dir, "reddit", "actions.jsonl")
if not platform or platform == "reddit":
actions.extend(cls._read_actions_from_file(
reddit_actions_log,
default_platform="reddit", # Automatically fill the platform field
platform_filter=platform,
agent_id=agent_id,
round_num=round_num
))
# Nếu thư mục chạy các nền tảng chạy parallel này (twitter / reddit) không có ở đó. Hãy thử với các tệp định dạng cũ
if not actions:
actions_log = os.path.join(sim_dir, "actions.jsonl")
actions = cls._read_actions_from_file(
actions_log,
default_platform=None, # Các file json log định dạng cũ đã có sẵn record về platform nên không điền default
platform_filter=platform,
agent_id=agent_id,
round_num=round_num
)
# Sắp xếp lại log theo thời gian timestamp giảm dần (từ mới hơn lên trước)
actions.sort(key=lambda x: x.timestamp, reverse=True)
return actions
@classmethod
def get_actions(
cls,
simulation_id: str,
limit: int = 100,
offset: int = 0,
platform: Optional[str] = None,
agent_id: Optional[int] = None,
round_num: Optional[int] = None
) -> List[AgentAction]:
"""
Lấy thông tin lịch sử diễn ra (Hỗ trợ phân trang bằng offset và limit)
Args:
simulation_id: Simulation ID
limit: Record Count returns limit
offset: Offset
platform: Filter Platform
agent_id: Filter Agent by ID
round_num: Filter round loop
Returns:
Actions list
"""
actions = cls.get_all_actions(
simulation_id=simulation_id,
platform=platform,
agent_id=agent_id,
round_num=round_num
)
# Phân trang
return actions[offset:offset + limit]
@classmethod
def get_timeline(
cls,
simulation_id: str,
start_round: int = 0,
end_round: Optional[int] = None
) -> List[Dict[str, Any]]:
"""
Lấy thời gian (timeline) mô phỏng diễn ra (tóm tắt theo vòng được khai báo)
Args:
simulation_id: ID Mô phỏng
start_round: Bắt đầu từ một vòng lặp nhất định (First round Number)
end_round: Kết thúc từ vòng ở đó (End round Number)
Returns:
Cung cấp đầy đủ thông tin về các round bị gói gọn
"""
actions = cls.get_actions(simulation_id, limit=10000)
# Nhóm tiến trình lại vào trong Vòng (round grouping)
rounds: Dict[int, Dict[str, Any]] = {}
for action in actions:
round_num = action.round_num
if round_num < start_round:
continue
if end_round is not None and round_num > end_round:
continue
if round_num not in rounds:
rounds[round_num] = {
"round_num": round_num,
"twitter_actions": 0,
"reddit_actions": 0,
"active_agents": set(),
"action_types": {},
"first_action_time": action.timestamp,
"last_action_time": action.timestamp,
}
r = rounds[round_num]
if action.platform == "twitter":
r["twitter_actions"] += 1
else:
r["reddit_actions"] += 1
r["active_agents"].add(action.agent_id)
r["action_types"][action.action_type] = r["action_types"].get(action.action_type, 0) + 1
r["last_action_time"] = action.timestamp
# Chuyển đổi trạng thái về Lists Arrays Data type
result = []
for round_num in sorted(rounds.keys()):
r = rounds[round_num]
result.append({
"round_num": round_num,
"twitter_actions": r["twitter_actions"],
"reddit_actions": r["reddit_actions"],
"total_actions": r["twitter_actions"] + r["reddit_actions"],
"active_agents_count": len(r["active_agents"]),
"active_agents": list(r["active_agents"]),
"action_types": r["action_types"],
"first_action_time": r["first_action_time"],
"last_action_time": r["last_action_time"],
})
return result
@classmethod
def get_agent_stats(cls, simulation_id: str) -> List[Dict[str, Any]]:
"""
Lấy thông kê của mọi agent
Returns:
Danh sách thống kê Agent
"""
actions = cls.get_actions(simulation_id, limit=10000)
agent_stats: Dict[int, Dict[str, Any]] = {}
for action in actions:
agent_id = action.agent_id
if agent_id not in agent_stats:
agent_stats[agent_id] = {
"agent_id": agent_id,
"agent_name": action.agent_name,
"total_actions": 0,
"twitter_actions": 0,
"reddit_actions": 0,
"action_types": {},
"first_action_time": action.timestamp,
"last_action_time": action.timestamp,
}
stats = agent_stats[agent_id]
stats["total_actions"] += 1
if action.platform == "twitter":
stats["twitter_actions"] += 1
else:
stats["reddit_actions"] += 1
stats["action_types"][action.action_type] = stats["action_types"].get(action.action_type, 0) + 1
stats["last_action_time"] = action.timestamp
# Sắp xếp theo tổng số hành động giảm dần (reverse = true)
result = sorted(agent_stats.values(), key=lambda x: x["total_actions"], reverse=True)
return result
@classmethod
def cleanup_simulation_logs(cls, simulation_id: str) -> Dict[str, Any]:
"""
Xóa tệp log chạy để buộc mô phỏng được khởi động lại
Xóa sạch các tệp tin này bao gồm:
- run_state.json
- twitter/actions.jsonl
- reddit/actions.jsonl
- simulation.log
- stdout.log / stderr.log
- twitter_simulation.dbDữ liệu nền tảng twitter)
- reddit_simulation.dbDữ liệu nền tảng reddit
- env_status.jsonTrạng thái file Environment status
Chú ý: Các file liên kết đến cấu hình mô phỏng hay config thiết lập (như là simulation_config.json) hay Profile đều sẽ KHÔNG bị xóa đi.
Args:
simulation_id: Simulation ID
Returns:
Kết quả của lệnh xóa sạch (clean up)
"""
import shutil
sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id)
if not os.path.exists(sim_dir):
return {"success": True, "message": "Simulation directory does not exist, no need to clean."}
cleaned_files = []
errors = []
# Các tệp tin cần bị loại bỏ bao gồm log, database...
files_to_delete = [
"run_state.json",
"simulation.log",
"stdout.log",
"stderr.log",
"twitter_simulation.db", # Twitter Database
"reddit_simulation.db", # Reddit Database
"env_status.json", # Env state status file
]
# Nhưng tệp tin có cấp quyền cần xóa (có liên quan nhật ký hoạt động actions.jsonl)
dirs_to_clean = ["twitter", "reddit"]
# Loại bỏ các tệp không cần tới (Delete them)
for filename in files_to_delete:
file_path = os.path.join(sim_dir, filename)
if os.path.exists(file_path):
try:
os.remove(file_path)
cleaned_files.append(filename)
except Exception as e:
errors.append(f"Failed to delete {filename}: {str(e)}")
# Kiểm tra lại các file theo thư mục chứa action history action.jsonl files
for dir_name in dirs_to_clean:
dir_path = os.path.join(sim_dir, dir_name)
if os.path.exists(dir_path):
actions_file = os.path.join(dir_path, "actions.jsonl")
if os.path.exists(actions_file):
try:
os.remove(actions_file)
cleaned_files.append(f"{dir_name}/actions.jsonl")
except Exception as e:
errors.append(f"Failed to delete {dir_name}/actions.jsonl: {str(e)}")
# Xóa (clear) cache nhớ run state
if simulation_id in cls._run_states:
del cls._run_states[simulation_id]
logger.info(f"Clean up complete for simulation: {simulation_id}, Deleted files: {cleaned_files}")
return {
"success": len(errors) == 0,
"cleaned_files": cleaned_files,
"errors": errors if errors else None
}
# Flags Ngăn việc phải làm quá nhiều việc cho một action cleanup đã làm từ đầu hay khi gọi lần tới lệnh giống
_cleanup_done = False
@classmethod
def cleanup_all_simulations(cls):
"""
Dọn dẹp tất cả các tiến trình mô phỏng đang chạy
Được gọi (call) khi đóng máy chủ, nhằm vào việc muốn các máy chủ con (child processes) bị tắt theo
"""
# Nếu đã clean (dọn dẹp) thì không làm nữa
if cls._cleanup_done:
return
cls._cleanup_done = True
# Kiểm tra xem có gì để dọn dẹp không (tránh log rỗng khi không có tiến trình chạy)
has_processes = bool(cls._processes)
has_updaters = bool(cls._graph_memory_enabled)
if not has_processes and not has_updaters:
return # Không có gì để dọn, kết thúc
logger.info("Cleaning up all simulation processes...")
# Dừng tất cả cái update đồ thị nhớ (Bộ nhớ Graph) (Stop_all ghi nhận ở bên trong)
try:
ZepGraphMemoryManager.stop_all()
except Exception as e:
logger.error(f"Failed to stop Zep Graph update daemon: {e}")
cls._graph_memory_enabled.clear()
# Tạo bản sao Dictionary dict() từ cls._processes.items để không bị hỏng List lúc lặp (iterating)
processes = list(cls._processes.items())
for simulation_id, process in processes:
try:
if process.poll() is None: # Process (Tiến trình) Vẫn đang chạy
logger.info(f"Terminating simulation process: {simulation_id}, pid={process.pid}")
try:
# Áp dụng giải pháp dừng liên nền tảng (Cross-platform termination method)
cls._terminate_process(process, simulation_id, timeout=5)
except (ProcessLookupError, OSError):
# Trong trường hợp có thể các process này đã biến mất ở đâu đó rồi, xóa một cách bắt buộc
try:
process.terminate()
process.wait(timeout=3)
except Exception:
process.kill()
# Cập nhật run_state.json
state = cls.get_run_state(simulation_id)
if state:
state.runner_status = RunnerStatus.STOPPED
state.twitter_running = False
state.reddit_running = False
state.completed_at = datetime.now().isoformat()
state.error = "Server shut down, simulation terminated."
cls._save_run_state(state)
# Đồng thời cập nhật trạng thái `stopped` cho tệp (file) state.json
try:
sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id)
state_file = os.path.join(sim_dir, "state.json")
logger.info(f"Attempting to update state.json: {state_file}")
if os.path.exists(state_file):
with open(state_file, 'r', encoding='utf-8') as f:
state_data = json.load(f)
state_data['status'] = 'stopped'
state_data['updated_at'] = datetime.now().isoformat()
with open(state_file, 'w', encoding='utf-8') as f:
json.dump(state_data, f, indent=2, ensure_ascii=False)
logger.info(f"Updated state.json status to 'stopped': {simulation_id}")
else:
logger.warning(f"state.json not found: {state_file}")
except Exception as state_err:
logger.warning(f"Failed to update state.json: {simulation_id}, error={state_err}")
except Exception as e:
logger.error(f"Failed to clean up process: {simulation_id}, error={e}")
# Đóng tất cả tệp xử lý file handles (Log file, Errors File)
for simulation_id, file_handle in list(cls._stdout_files.items()):
try:
if file_handle:
file_handle.close()
except Exception:
pass
cls._stdout_files.clear()
for simulation_id, file_handle in list(cls._stderr_files.items()):
try:
if file_handle:
file_handle.close()
except Exception:
pass
cls._stderr_files.clear()
# Dọn dẹp trạng thái ở trong Ram Memory
cls._processes.clear()
cls._action_queues.clear()
logger.info("Simulation process clean up completed.")
@classmethod
def register_cleanup(cls):
"""
Đăng ký một lệnh Dọn dẹp (Cleanup command)
Trong lúc chuẩn bị khởi tạo App Flask, mình sẽ thiết lập nó sao cho gọi là máy chủ kết thúc (tắt) mọi quá trình (Simulation Process)
"""
global _cleanup_registered
if _cleanup_registered:
return
# Flask ở trong cơ chế gỡ rối `debug`, lúc này chỉ đăng ký ứng dụng để cho thằng app.run làm (Werkzeug)
# WERKZEUG_RUN_MAIN=true Đại diện quá trình tiến trình máy chủ được nạp lại
# Nhưng nó sẽ ko apply điều này ở Production nếu app không debug
is_reloader_process = os.environ.get('WERKZEUG_RUN_MAIN') == 'true'
is_debug_mode = os.environ.get('FLASK_DEBUG') == '1' or os.environ.get('WERKZEUG_RUN_MAIN') is not None
# Trong DebugMode, chúng ta chỉ cho phép re-loader Child-process chạy. Production vẫn luôn phải chạy process này
if is_debug_mode and not is_reloader_process:
_cleanup_registered = True # Check list đã lưu lại. Hủy quyền yêu cầu thêm
return
# Lưu các tín hiệu để trả về Signal handling sau khi dừng Process hoàn tất
original_sigint = signal.getsignal(signal.SIGINT)
original_sigterm = signal.getsignal(signal.SIGTERM)
# SIGHUP Chỉ xuất hiện trong Unix (Mac/Linux), Windows không có cái này
original_sighup = None
has_sighup = hasattr(signal, 'SIGHUP')
if has_sighup:
original_sighup = signal.getsignal(signal.SIGHUP)
def cleanup_handler(signum=None, frame=None):
"""Xử lý điều hướng tín hiệu (Signal Routing): Bắt đầu dọn tiến trình xong gởi lệnh báo (Original Processing Router)"""
# Chỉ báo nhật kí (log) nếu có tiến trình (Process) cần xử lý
if cls._processes or cls._graph_memory_enabled:
logger.info(f"Received signal {signum}, starting clean up...")
cls.cleanup_all_simulations()
# Gửi tín hiệu gọi hàm báo (handling functions) lúc đấy của Flask => App được tự do ngắt điện
if signum == signal.SIGINT and callable(original_sigint):
original_sigint(signum, frame)
elif signum == signal.SIGTERM and callable(original_sigterm):
original_sigterm(signum, frame)
elif has_sighup and signum == signal.SIGHUP:
# SIGHUP: Được trả về khi máy chủ bị dừng (Terminal Closed)
if callable(original_sighup):
original_sighup(signum, frame)
else:
# Mặc định hành vi: Đóng bình thường => sys.exit(0) "Tạm biệt các hành khách"
sys.exit(0)
else:
# Hành động ở cơ sở gốc (Root) không gọi được (SIG_DFL) => Hãy phát cảnh báo
raise KeyboardInterrupt
# Một phương án khác nếu tín hiệu đăng ký xử lý gặp khó (Fallback Option)
atexit.register(cls.cleanup_all_simulations)
# Đăng ký quản lý báo tín hiệu (Chỉ riêng trong chủ / main thread có cái luồng)
try:
# SIGTERM: Tín hiệu gốc của Kill Server (Linux/Mac)
signal.signal(signal.SIGTERM, cleanup_handler)
# SIGINT: Bấm lệnh Control + C / Ctrl+C
signal.signal(signal.SIGINT, cleanup_handler)
# SIGHUP: Máy bị đóng (Unix OS)
if has_sighup:
signal.signal(signal.SIGHUP, cleanup_handler)
except ValueError:
# Không ở MainThread => Sử dụng được duy nhất fallback Atexit
logger.warning("Failed to register signal handlers (not in main thread). Falling back to atexit.")
_cleanup_registered = True
@classmethod
def get_running_simulations(cls) -> List[str]:
"""
Lấy danh sách tất cả các ID của các phiên mô phỏng đang hoạt động
"""
running = []
for sim_id, process in cls._processes.items():
if process.poll() is None:
running.append(sim_id)
return running
# ============== Tính năng Phỏng vấn (Interview) ==============
@classmethod
def check_env_alive(cls, simulation_id: str) -> bool:
"""
Kiểm tra xem environment còn sống không (có thể nhận lệnh Interview)
Args:
simulation_id: Simulation ID
Returns:
True Nếu environment còn sống, False nghĩa là đã đóng
"""
sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id)
if not os.path.exists(sim_dir):
return False
ipc_client = SimulationIPCClient(sim_dir)
return ipc_client.check_env_alive()
@classmethod
def get_env_status_detail(cls, simulation_id: str) -> Dict[str, Any]:
"""
Lấy thông tin chi tiết về trạng thái của environment
Args:
simulation_id: Mô phỏng ID
Returns:
Bảng trạng thái chi tiết (Dictionary) bao gồm: status, twitter_available, reddit_available, timestamp
"""
sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id)
status_file = os.path.join(sim_dir, "env_status.json")
default_status = {
"status": "stopped",
"twitter_available": False,
"reddit_available": False,
"timestamp": None
}
if not os.path.exists(status_file):
return default_status
try:
with open(status_file, 'r', encoding='utf-8') as f:
status = json.load(f)
return {
"status": status.get("status", "stopped"),
"twitter_available": status.get("twitter_available", False),
"reddit_available": status.get("reddit_available", False),
"timestamp": status.get("timestamp")
}
except (json.JSONDecodeError, OSError):
return default_status
@classmethod
def interview_agent(
cls,
simulation_id: str,
agent_id: int,
prompt: str,
platform: str = None,
timeout: float = 60.0
) -> Dict[str, Any]:
"""
Phỏng vấn trên 1 Agent
Args:
simulation_id: ID mô phỏng
agent_id: Agent ID
prompt: Câu hỏi phỏng vấn
platform: Chỉ định nền tảng (Tùy chọn/Optional)
- "twitter": Chỉ PV trên account Twitter
- "reddit": Chỉ PV trên account Reddit
- None: Phỏng vấn chéo trên cả hai nền tảng, trả về kết quả hợp lại (Nếu chạy mô phỏng nền tảng kép)
timeout: Thời gian chờ tối đa (giây)
Returns:
Từ điền chứa kết quả PV
Raises:
ValueError: Không có mô phỏng hoặc environment ko chạy
TimeoutError: Đang chờ phản hồi bị timeout
"""
sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id)
if not os.path.exists(sim_dir):
raise ValueError(f"Simulation does not exist: {simulation_id}")
ipc_client = SimulationIPCClient(sim_dir)
if not ipc_client.check_env_alive():
raise ValueError(f"Simulation environment is not running or closed, cannot perform Interview: {simulation_id}")
logger.info(f"Sending Interview Command: simulation_id={simulation_id}, agent_id={agent_id}, platform={platform}")
response = ipc_client.send_interview(
agent_id=agent_id,
prompt=prompt,
platform=platform,
timeout=timeout
)
if response.status.value == "completed":
return {
"success": True,
"agent_id": agent_id,
"prompt": prompt,
"result": response.result,
"timestamp": response.timestamp
}
else:
return {
"success": False,
"agent_id": agent_id,
"prompt": prompt,
"error": response.error,
"timestamp": response.timestamp
}
@classmethod
def interview_agents_batch(
cls,
simulation_id: str,
interviews: List[Dict[str, Any]],
platform: str = None,
timeout: float = 120.0
) -> Dict[str, Any]:
"""
Phỏng vấn hàng loạt nhiều Agent
Args:
simulation_id: ID mô phỏng
interviews: Danh sách nội dung phỏng vấn, mỗi phần tử (element) chứa {"agent_id": int, "prompt": str, "platform": str(tùy chọn)}
platform: Nền tảng mặc định (Nếu không chọn riêng cho từng phần tử)
- "twitter": Mặc định chỉ dùng mạng Twitter
- "reddit": Mặc định chỉ dùng mạng Reddit
- None: Phỏng vấn gộp trên cả hai nền tảng với mỗi Agent
timeout: Hết thời gian chờ (ms) (seconds)
Returns:
Dict từ điển với các kết quả phỏng vấn hàng loạt
Raises:
ValueError: Chưa có tiến trình chạy mô phỏng
TimeoutError: Phỏng vấn lâu quá (Timeout timeout timeout)
"""
sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id)
if not os.path.exists(sim_dir):
raise ValueError(f"Simulation does not exist: {simulation_id}")
ipc_client = SimulationIPCClient(sim_dir)
if not ipc_client.check_env_alive():
raise ValueError(f"Simulation environment is not running or closed, cannot perform Interview: {simulation_id}")
logger.info(f"Sending batch Interview command: simulation_id={simulation_id}, count={len(interviews)}, platform={platform}")
response = ipc_client.send_batch_interview(
interviews=interviews,
platform=platform,
timeout=timeout
)
if response.status.value == "completed":
return {
"success": True,
"interviews_count": len(interviews),
"result": response.result,
"timestamp": response.timestamp
}
else:
return {
"success": False,
"interviews_count": len(interviews),
"error": response.error,
"timestamp": response.timestamp
}
@classmethod
def interview_all_agents(
cls,
simulation_id: str,
prompt: str,
platform: str = None,
timeout: float = 180.0
) -> Dict[str, Any]:
"""
Phỏng vấn TOÀN BỘ Agent (Phỏng vấn tổng quan - Global interview)
Hỏi một câu hỏi với toàn bộ Agent đang có trong phiên mô phỏng hiện tại
Args:
simulation_id: ID mô phỏng
prompt: Câu hỏi (Cho tất cả các Agent)
platform: Quyết định nền tảng (Platform decision)
- "twitter": Trỏ tới Twitter Platform
- "reddit": Trỏ tới Reddit Platform
- None: Interview kết hợp trên cả nền tảng của từng agent
timeout: Timeout
Returns:
Kết quả của toàn thể hội đồng Agents (Lớp/Nhóm)
"""
sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id)
if not os.path.exists(sim_dir):
raise ValueError(f"Simulation does not exist: {simulation_id}")
# Fetch All Agents profile (Lấy thông tin agents từ phần thiết lập)
config_path = os.path.join(sim_dir, "simulation_config.json")
if not os.path.exists(config_path):
raise ValueError(f"Simulation config not found: {simulation_id}")
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
agent_configs = config.get("agent_configs", [])
if not agent_configs:
raise ValueError(f"No Agent defined under simulation configs: {simulation_id}")
# Tập hợp danh sách phỏng vấn tất cả
interviews = []
for agent_config in agent_configs:
agent_id = agent_config.get("agent_id")
if agent_id is not None:
interviews.append({
"agent_id": agent_id,
"prompt": prompt
})
logger.info(f"Sending GLOBAL Interview command: simulation_id={simulation_id}, agent_count={len(interviews)}, platform={platform}")
return cls.interview_agents_batch(
simulation_id=simulation_id,
interviews=interviews,
platform=platform,
timeout=timeout
)
@classmethod
def close_simulation_env(
cls,
simulation_id: str,
timeout: float = 30.0
) -> Dict[str, Any]:
"""
Đóng Environment giả lập (Không phải dừng process tắt hẳn nó đi)
Gửi lệnh ra hiệu cho Simulation ngắt bỏ tiến trình để các processes thoát ra an toàn và êm đẹp về trạng thái đang chờ nhận lệnh
Args:
simulation_id: ID Simulation
timeout: Timeout chờ kết nối
Returns:
Kiểu từ điển: Quá trình (Process Status execution)
"""
sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id)
if not os.path.exists(sim_dir):
raise ValueError(f"Simulation does not exist: {simulation_id}")
ipc_client = SimulationIPCClient(sim_dir)
if not ipc_client.check_env_alive():
return {
"success": True,
"message": "Environment is already closed"
}
logger.info(f"Sending command to close Environment: simulation_id={simulation_id}")
try:
response = ipc_client.send_close_env(timeout=timeout)
return {
"success": response.status.value == "completed",
"message": "Environment close command sent",
"result": response.result,
"timestamp": response.timestamp
}
except TimeoutError:
# Hết thời gian chờ nguyên nhân lớn nhất là vì Simulation environment đang đóng giữa chừng.
return {
"success": True,
"message": "Environment close command sent (timeout waiting for response, env might be closing)"
}
except Exception as e:
return {
"success": False,
"message": f"Failed to send close env command: {str(e)}"
}
@classmethod
def _get_interview_history_from_db(
cls,
db_path: str,
platform_name: str,
agent_id: Optional[int] = None,
limit: int = 100
) -> List[Dict[str, Any]]:
"""Lấy lịch sử phỏng vấn từ Local Database của nền tảng"""
import sqlite3
if not os.path.exists(db_path):
return []
results = []
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
if agent_id is not None:
cursor.execute("""
SELECT user_id, info, created_at
FROM trace
WHERE action = 'interview' AND user_id = ?
ORDER BY created_at DESC
LIMIT ?
""", (agent_id, limit))
else:
cursor.execute("""
SELECT user_id, info, created_at
FROM trace
WHERE action = 'interview'
ORDER BY created_at DESC
LIMIT ?
""", (limit,))
for user_id, info_json, created_at in cursor.fetchall():
try:
info = json.loads(info_json) if info_json else {}
except json.JSONDecodeError:
info = {"raw": info_json}
results.append({
"agent_id": user_id,
"response": info.get("response", info),
"prompt": info.get("prompt", ""),
"timestamp": created_at,
"platform": platform_name
})
conn.close()
except Exception as e:
logger.error(f"Failed to load Interview history ({platform_name}): {e}")
return results
@classmethod
def get_interview_history(
cls,
simulation_id: str,
platform: str = None,
agent_id: Optional[int] = None,
limit: int = 100
) -> List[Dict[str, Any]]:
"""
Lịch sử lấy danh sách câu trả lời của các câu hỏi với agents (Đọc từ DataBase db)
Args:
simulation_id: Nhận dạng ID cho mỗi Simulation
platform: Chị định Nền tảng (reddit/twitter/None)
- "reddit": Chỉ trên reddit
- "twitter": Chỉ lấy records ghi được trên mạng xã hội twitter giả lập
- None: Kết hợp lấy logs của cả hai social network
agent_id: Cung cấp tùy chọn cho loại Agent qua ID
limit: Lượng dữ liệu load tối đa cho 1 request get query trên 1 nền tảng
Returns:
Danh sách lưu vết record lịch sử phỏng vấn của các agents
"""
sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id)
results = []
# Xác nhận nền tảng cung cấp cho truy vấn
if platform in ("reddit", "twitter"):
platforms = [platform]
else:
# Nếu người dùng để trống, có nghĩa là gọi tất cả kết quả
platforms = ["twitter", "reddit"]
for p in platforms:
db_path = os.path.join(sim_dir, f"{p}_simulation.db")
platform_results = cls._get_interview_history_from_db(
db_path=db_path,
platform_name=p,
agent_id=agent_id,
limit=limit
)
results.extend(platform_results)
# Sắp xếp chúng lại bằng thời gian gần nhất lên trước
results.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
# Cho trường hợp query kết hợp nhiều nền tảng, phải tiến hành gọt lấy đúng 1 giới hạn nhất định
if len(platforms) > 1 and len(results) > limit:
results = results[:limit]
return results