395 lines
13 KiB
Python
395 lines
13 KiB
Python
"""
|
|
Module Giao tiếp IPC của Mô phỏng
|
|
Dùng cho giao tiếp liên tiến trình giữa backend Flask và file script mô phỏng
|
|
|
|
Cấu trúc lệnh/phản hồi đơn giản được hiện thực hóa thông qua hệ thống tệp:
|
|
1. Flask ghi lệnh vào thư mục commands/
|
|
2. Kịch bản mô phỏng thăm dò (poll) thư mục lệnh, thực thi lệnh và ghi chuỗi phản hồi vào thư mục responses/
|
|
3. Flask thăm dò lại thư mục phản hồi để nhận kết quả
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import time
|
|
import uuid
|
|
from typing import Dict, Any, Optional, List
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
|
|
from ..utils.logger import get_logger
|
|
|
|
logger = get_logger('mirofish.simulation_ipc')
|
|
|
|
|
|
class CommandType(str, Enum):
|
|
"""Các loại lệnh (command)"""
|
|
INTERVIEW = "interview" # Phỏng vấn agent đơn lẻ
|
|
BATCH_INTERVIEW = "batch_interview" # Phỏng vấn hàng loạt
|
|
CLOSE_ENV = "close_env" # Đóng môi trường
|
|
|
|
|
|
class CommandStatus(str, Enum):
|
|
"""Trạng thái của các lệnh (command)"""
|
|
PENDING = "pending"
|
|
PROCESSING = "processing"
|
|
COMPLETED = "completed"
|
|
FAILED = "failed"
|
|
|
|
|
|
@dataclass
|
|
class IPCCommand:
|
|
"""Lệnh (Command) IPC"""
|
|
command_id: str
|
|
command_type: CommandType
|
|
args: Dict[str, Any]
|
|
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"command_id": self.command_id,
|
|
"command_type": self.command_type.value,
|
|
"args": self.args,
|
|
"timestamp": self.timestamp
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'IPCCommand':
|
|
return cls(
|
|
command_id=data["command_id"],
|
|
command_type=CommandType(data["command_type"]),
|
|
args=data.get("args", {}),
|
|
timestamp=data.get("timestamp", datetime.now().isoformat())
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class IPCResponse:
|
|
"""Phản hồi (Response) IPC"""
|
|
command_id: str
|
|
status: CommandStatus
|
|
result: Optional[Dict[str, Any]] = None
|
|
error: Optional[str] = None
|
|
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"command_id": self.command_id,
|
|
"status": self.status.value,
|
|
"result": self.result,
|
|
"error": self.error,
|
|
"timestamp": self.timestamp
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'IPCResponse':
|
|
return cls(
|
|
command_id=data["command_id"],
|
|
status=CommandStatus(data["status"]),
|
|
result=data.get("result"),
|
|
error=data.get("error"),
|
|
timestamp=data.get("timestamp", datetime.now().isoformat())
|
|
)
|
|
|
|
|
|
class SimulationIPCClient:
|
|
"""
|
|
Client (Máy khách) IPC Mô phỏng (dùng phía Flask)
|
|
|
|
Được dùng để gửi file tới tiến trình mô phỏng và chờ response trả về
|
|
"""
|
|
|
|
def __init__(self, simulation_dir: str):
|
|
"""
|
|
Khởi tạo Client IPC
|
|
|
|
Args:
|
|
simulation_dir: Thư mục chứa dữ liệu mô phỏng
|
|
"""
|
|
self.simulation_dir = simulation_dir
|
|
self.commands_dir = os.path.join(simulation_dir, "ipc_commands")
|
|
self.responses_dir = os.path.join(simulation_dir, "ipc_responses")
|
|
|
|
# Đảm bảo rằng thư mục đã tồn tại
|
|
os.makedirs(self.commands_dir, exist_ok=True)
|
|
os.makedirs(self.responses_dir, exist_ok=True)
|
|
|
|
def send_command(
|
|
self,
|
|
command_type: CommandType,
|
|
args: Dict[str, Any],
|
|
timeout: float = 60.0,
|
|
poll_interval: float = 0.5
|
|
) -> IPCResponse:
|
|
"""
|
|
Gửi lệnh ra và đợi kết quả phản hồi lại
|
|
|
|
Args:
|
|
command_type: Loại lệnh
|
|
args: Tham số của lệnh
|
|
timeout: Thời gian timeout (giây)
|
|
poll_interval: Khoảng thời gian giữa các lần thăm dò (giây)
|
|
|
|
Returns:
|
|
IPCResponse
|
|
|
|
Raises:
|
|
TimeoutError: Lỗi quá thời gian chờ phản hồi
|
|
"""
|
|
command_id = str(uuid.uuid4())
|
|
command = s(
|
|
command_id=command_id,
|
|
command_type=command_type,
|
|
args=args
|
|
)
|
|
|
|
# Ghi vào file lệnh
|
|
command_file = os.path.join(self.commands_dir, f"{command_id}.json")
|
|
with open(command_file, 'w', encoding='utf-8') as f:
|
|
json.dump(command.to_dict(), f, ensure_ascii=False, indent=2)
|
|
|
|
logger.info(f"Send IPC command: {command_type.value}, command_id={command_id}")
|
|
|
|
# Chờ kết quả phản hồi
|
|
response_file = os.path.join(self.responses_dir, f"{command_id}.json")
|
|
start_time = time.time()
|
|
|
|
while time.time() - start_time < timeout:
|
|
if os.path.exists(response_file):
|
|
try:
|
|
with open(response_file, 'r', encoding='utf-8') as f:
|
|
response_data = json.load(f)
|
|
response = IPCResponse.from_dict(response_data)
|
|
|
|
# Xóa file lệnh và file phản hồi đi
|
|
try:
|
|
os.remove(command_file)
|
|
os.remove(response_file)
|
|
except OSError:
|
|
pass
|
|
|
|
logger.info(f"Received IPC response: command_id={command_id}, status={response.status.value}")
|
|
return response
|
|
except (json.JSONDecodeError, KeyError) as e:
|
|
logger.warning(f"Failed to parse response: {e}")
|
|
|
|
time.sleep(poll_interval)
|
|
|
|
# Timed out
|
|
logger.error(f"Timeout waiting for IPC response: command_id={command_id}")
|
|
|
|
# Xóa file lệnh đi
|
|
try:
|
|
os.remove(command_file)
|
|
except OSError:
|
|
pass
|
|
|
|
raise TimeoutError(f"Wait command response timed out ({timeout} seconds)")
|
|
|
|
def send_interview(
|
|
self,
|
|
agent_id: int,
|
|
prompt: str,
|
|
platform: str = None,
|
|
timeout: float = 60.0
|
|
) -> IPCResponse:
|
|
"""
|
|
Gửi lệnh phỏng vấn agent đơn lẻ
|
|
|
|
Args:
|
|
agent_id: Agent ID
|
|
prompt: Câu hỏi phỏng vấn
|
|
platform: Chỉ định nền tảng (Tùy chọn)
|
|
- "twitter": Chỉ phỏng vấn ở nền tảng Twitter
|
|
- "reddit": Chỉ phỏng vấn ở nền tảng Reddit
|
|
- None: Phỏng vấn đồng thời cả hai nền tảng khi mô phỏng nền tảng kép, phỏng vấn một nền tảng đó khi mô phỏng nền tảng đơn
|
|
timeout: Thời gian timeout
|
|
|
|
Returns:
|
|
IPCResponse, trong đó trường result sẽ chứa kết quả cuộc phỏng vấn
|
|
"""
|
|
args = {
|
|
"agent_id": agent_id,
|
|
"prompt": prompt
|
|
}
|
|
if platform:
|
|
args["platform"] = platform
|
|
|
|
return self.send_command(
|
|
command_type=CommandType.INTERVIEW,
|
|
args=args,
|
|
timeout=timeout
|
|
)
|
|
|
|
def send_batch_interview(
|
|
self,
|
|
interviews: List[Dict[str, Any]],
|
|
platform: str = None,
|
|
timeout: float = 120.0
|
|
) -> IPCResponse:
|
|
"""
|
|
Gửi lệnh phỏng vấn hàng loạt
|
|
|
|
Args:
|
|
interviews: Danh sách phỏng vấn, mỗi phần tử chứa {"agent_id": int, "prompt": str, "platform": str(Tùy chọn)}
|
|
platform: Nền tảng mặc định (Tùy chọn, sẽ bị ghi đè bởi "platform" của từng mục phỏng vấn riêng lẻ)
|
|
- "twitter": Mặc định chỉ phỏng vấn ở nền tảng Twitter
|
|
- "reddit": Mặc định chỉ phỏng vấn ở nền tảng Reddit
|
|
- None: Mỗi Agent sẽ được phỏng vấn đồng thời trên cả hai nền tảng khi mô phỏng nền tảng kép
|
|
timeout: Thời gian timeout
|
|
|
|
Returns:
|
|
IPCResponse, trong đó trường result sẽ chứa tất cả các kết quả phỏng vấn
|
|
"""
|
|
args = {"interviews": interviews}
|
|
if platform:
|
|
args["platform"] = platform
|
|
|
|
return self.send_command(
|
|
command_type=CommandType.BATCH_INTERVIEW,
|
|
args=args,
|
|
timeout=timeout
|
|
)
|
|
|
|
def send_close_env(self, timeout: float = 30.0) -> IPCResponse:
|
|
"""
|
|
Gửi lệnh đóng môi trường
|
|
|
|
Args:
|
|
timeout: Thời gian timeout
|
|
|
|
Returns:
|
|
IPCResponse
|
|
"""
|
|
return self.send_command(
|
|
command_type=CommandType.CLOSE_ENV,
|
|
args={},
|
|
timeout=timeout
|
|
)
|
|
|
|
def check_env_alive(self) -> bool:
|
|
"""
|
|
Kiểm tra xem môi trường mô phỏng còn sống hay không
|
|
|
|
Được xác định thông qua việc kiểm tra tệp tin env_status.json
|
|
"""
|
|
status_file = os.path.join(self.simulation_dir, "env_status.json")
|
|
if not os.path.exists(status_file):
|
|
return False
|
|
|
|
try:
|
|
with open(status_file, 'r', encoding='utf-8') as f:
|
|
status = json.load(f)
|
|
return status.get("status") == "alive"
|
|
except (json.JSONDecodeError, OSError):
|
|
return False
|
|
|
|
|
|
class SimulationIPCServer:
|
|
"""
|
|
Server (Máy chủ) IPC Mô phỏng (dùng phía kịch bản mô phỏng)
|
|
|
|
Tiến hành thăm dò thư mục lệnh, thực thi lệnh và trả về kết quả phản hồi
|
|
"""
|
|
|
|
def __init__(self, simulation_dir: str):
|
|
"""
|
|
Khởi tạo Máy chủ IPC
|
|
|
|
Args:
|
|
simulation_dir: Thư mục chứa dữ liệu mô phỏng
|
|
"""
|
|
self.simulation_dir = simulation_dir
|
|
self.commands_dir = os.path.join(simulation_dir, "ipc_commands")
|
|
self.responses_dir = os.path.join(simulation_dir, "ipc_responses")
|
|
|
|
# Đảm bảo rằng thư mục đã tồn tại
|
|
os.makedirs(self.commands_dir, exist_ok=True)
|
|
os.makedirs(self.responses_dir, exist_ok=True)
|
|
|
|
# Trạng thái môi trường
|
|
self._running = False
|
|
|
|
def start(self):
|
|
"""Đánh dấu Máy chủ đang ở trạng thái chạy"""
|
|
self._running = True
|
|
self._update_env_status("alive")
|
|
|
|
def stop(self):
|
|
"""Đánh dấu Máy chủ đang ở trạng thái dừng"""
|
|
self._running = False
|
|
self._update_env_status("stopped")
|
|
|
|
def _update_env_status(self, status: str):
|
|
"""Cập nhật tệp trạng thái môi trường"""
|
|
status_file = os.path.join(self.simulation_dir, "env_status.json")
|
|
with open(status_file, 'w', encoding='utf-8') as f:
|
|
json.dump({
|
|
"status": status,
|
|
"timestamp": datetime.now().isoformat()
|
|
}, f, ensure_ascii=False, indent=2)
|
|
|
|
def poll_commands(self) -> Optional[IPCCommand]:
|
|
"""
|
|
Thăm dò thư mục lệnh, trả về lệnh chờ xử lý đầu tiên
|
|
|
|
Returns:
|
|
IPCCommand hoặc None
|
|
"""
|
|
if not os.path.exists(self.commands_dir):
|
|
return None
|
|
|
|
# Lấy danh sách file lệnh và sắp xếp theo thời gian
|
|
command_files = []
|
|
for filename in os.listdir(self.commands_dir):
|
|
if filename.endswith('.json'):
|
|
filepath = os.path.join(self.commands_dir, filename)
|
|
command_files.append((filepath, os.path.getmtime(filepath)))
|
|
|
|
command_files.sort(key=lambda x: x[1])
|
|
|
|
for filepath, _ in command_files:
|
|
try:
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
return IPCCommand.from_dict(data)
|
|
except (json.JSONDecodeError, KeyError, OSError) as e:
|
|
logger.warning(f"Failed to read command file: {filepath}, {e}")
|
|
continue
|
|
|
|
return None
|
|
|
|
def send_response(self, response: IPCResponse):
|
|
"""
|
|
Gửi phản hồi
|
|
|
|
Args:
|
|
response: Phản hồi IPC
|
|
"""
|
|
response_file = os.path.join(self.responses_dir, f"{response.command_id}.json")
|
|
with open(response_file, 'w', encoding='utf-8') as f:
|
|
json.dump(response.to_dict(), f, ensure_ascii=False, indent=2)
|
|
|
|
# Xóa file lệnh đi
|
|
command_file = os.path.join(self.commands_dir, f"{response.command_id}.json")
|
|
try:
|
|
os.remove(command_file)
|
|
except OSError:
|
|
pass
|
|
|
|
def send_success(self, command_id: str, result: Dict[str, Any]):
|
|
"""Gửi phản hồi thành công"""
|
|
self.send_response(IPCResponse(
|
|
command_id=command_id,
|
|
status=CommandStatus.COMPLETED,
|
|
result=result
|
|
))
|
|
|
|
def send_error(self, command_id: str, error: str):
|
|
"""Gửi phản hồi lỗi"""
|
|
self.send_response(IPCResponse(
|
|
command_id=command_id,
|
|
status=CommandStatus.FAILED,
|
|
error=error
|
|
))
|