307 lines
10 KiB
Python
307 lines
10 KiB
Python
"""Action logger.
|
|
|
|
Records each agent action during an OASIS simulation so the backend can
|
|
monitor progress.
|
|
|
|
Log layout::
|
|
|
|
sim_xxx/
|
|
├── twitter/
|
|
│ └── actions.jsonl # Twitter action log
|
|
├── reddit/
|
|
│ └── actions.jsonl # Reddit action log
|
|
├── simulation.log # main simulation process log
|
|
└── run_state.json # run state (queried by the API)
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Dict, Any, Optional
|
|
|
|
|
|
class PlatformActionLogger:
|
|
"""Per-platform action logger."""
|
|
|
|
def __init__(self, platform: str, base_dir: str):
|
|
"""Initialize the logger.
|
|
|
|
Args:
|
|
platform: Platform name (``twitter`` or ``reddit``).
|
|
base_dir: Base path of the simulation directory.
|
|
"""
|
|
self.platform = platform
|
|
self.base_dir = base_dir
|
|
self.log_dir = os.path.join(base_dir, platform)
|
|
self.log_path = os.path.join(self.log_dir, "actions.jsonl")
|
|
self._ensure_dir()
|
|
|
|
def _ensure_dir(self):
|
|
"""Ensure the log directory exists."""
|
|
os.makedirs(self.log_dir, exist_ok=True)
|
|
|
|
def log_action(
|
|
self,
|
|
round_num: int,
|
|
agent_id: int,
|
|
agent_name: str,
|
|
action_type: str,
|
|
action_args: Optional[Dict[str, Any]] = None,
|
|
result: Optional[str] = None,
|
|
success: bool = True
|
|
):
|
|
"""Append a single action record."""
|
|
entry = {
|
|
"round": round_num,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"agent_id": agent_id,
|
|
"agent_name": agent_name,
|
|
"action_type": action_type,
|
|
"action_args": action_args or {},
|
|
"result": result,
|
|
"success": success,
|
|
}
|
|
|
|
with open(self.log_path, 'a', encoding='utf-8') as f:
|
|
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
|
|
|
def log_round_start(self, round_num: int, simulated_hour: int):
|
|
"""Append a round-start marker."""
|
|
entry = {
|
|
"round": round_num,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"event_type": "round_start",
|
|
"simulated_hour": simulated_hour,
|
|
}
|
|
|
|
with open(self.log_path, 'a', encoding='utf-8') as f:
|
|
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
|
|
|
def log_round_end(self, round_num: int, actions_count: int):
|
|
"""Append a round-end marker."""
|
|
entry = {
|
|
"round": round_num,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"event_type": "round_end",
|
|
"actions_count": actions_count,
|
|
}
|
|
|
|
with open(self.log_path, 'a', encoding='utf-8') as f:
|
|
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
|
|
|
def log_simulation_start(self, config: Dict[str, Any]):
|
|
"""Append a simulation-start marker."""
|
|
entry = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"event_type": "simulation_start",
|
|
"platform": self.platform,
|
|
"total_rounds": config.get("time_config", {}).get("total_simulation_hours", 72) * 2,
|
|
"agents_count": len(config.get("agent_configs", [])),
|
|
}
|
|
|
|
with open(self.log_path, 'a', encoding='utf-8') as f:
|
|
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
|
|
|
def log_simulation_end(self, total_rounds: int, total_actions: int):
|
|
"""Append a simulation-end marker."""
|
|
entry = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"event_type": "simulation_end",
|
|
"platform": self.platform,
|
|
"total_rounds": total_rounds,
|
|
"total_actions": total_actions,
|
|
}
|
|
|
|
with open(self.log_path, 'a', encoding='utf-8') as f:
|
|
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
|
|
|
|
|
class SimulationLogManager:
|
|
"""Top-level log manager.
|
|
|
|
Owns and dispatches to the per-platform action loggers, and exposes a
|
|
main process logger for non-action messages.
|
|
"""
|
|
|
|
def __init__(self, simulation_dir: str):
|
|
"""Initialize the log manager.
|
|
|
|
Args:
|
|
simulation_dir: Path to the simulation directory.
|
|
"""
|
|
self.simulation_dir = simulation_dir
|
|
self.twitter_logger: Optional[PlatformActionLogger] = None
|
|
self.reddit_logger: Optional[PlatformActionLogger] = None
|
|
self._main_logger: Optional[logging.Logger] = None
|
|
|
|
# Configure the main process logger.
|
|
self._setup_main_logger()
|
|
|
|
def _setup_main_logger(self):
|
|
"""Configure the main simulation log."""
|
|
log_path = os.path.join(self.simulation_dir, "simulation.log")
|
|
|
|
# Build the logger.
|
|
self._main_logger = logging.getLogger(f"simulation.{os.path.basename(self.simulation_dir)}")
|
|
self._main_logger.setLevel(logging.INFO)
|
|
self._main_logger.handlers.clear()
|
|
|
|
# File handler.
|
|
file_handler = logging.FileHandler(log_path, encoding='utf-8', mode='w')
|
|
file_handler.setLevel(logging.INFO)
|
|
file_handler.setFormatter(logging.Formatter(
|
|
'%(asctime)s - %(levelname)s - %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
|
))
|
|
self._main_logger.addHandler(file_handler)
|
|
|
|
# Console handler.
|
|
console_handler = logging.StreamHandler()
|
|
console_handler.setLevel(logging.INFO)
|
|
console_handler.setFormatter(logging.Formatter(
|
|
'[%(asctime)s] %(message)s',
|
|
datefmt='%H:%M:%S'
|
|
))
|
|
self._main_logger.addHandler(console_handler)
|
|
|
|
self._main_logger.propagate = False
|
|
|
|
def get_twitter_logger(self) -> PlatformActionLogger:
|
|
"""Lazily construct and return the Twitter platform logger."""
|
|
if self.twitter_logger is None:
|
|
self.twitter_logger = PlatformActionLogger("twitter", self.simulation_dir)
|
|
return self.twitter_logger
|
|
|
|
def get_reddit_logger(self) -> PlatformActionLogger:
|
|
"""Lazily construct and return the Reddit platform logger."""
|
|
if self.reddit_logger is None:
|
|
self.reddit_logger = PlatformActionLogger("reddit", self.simulation_dir)
|
|
return self.reddit_logger
|
|
|
|
def log(self, message: str, level: str = "info"):
|
|
"""Forward a message to the main logger at the given level."""
|
|
if self._main_logger:
|
|
getattr(self._main_logger, level.lower(), self._main_logger.info)(message)
|
|
|
|
def info(self, message: str):
|
|
self.log(message, "info")
|
|
|
|
def warning(self, message: str):
|
|
self.log(message, "warning")
|
|
|
|
def error(self, message: str):
|
|
self.log(message, "error")
|
|
|
|
def debug(self, message: str):
|
|
self.log(message, "debug")
|
|
|
|
|
|
# ============ Legacy interface ============
|
|
|
|
class ActionLogger:
|
|
"""Legacy single-platform action logger.
|
|
|
|
Prefer :class:`SimulationLogManager` for new code.
|
|
"""
|
|
|
|
def __init__(self, log_path: str):
|
|
self.log_path = log_path
|
|
self._ensure_dir()
|
|
|
|
def _ensure_dir(self):
|
|
log_dir = os.path.dirname(self.log_path)
|
|
if log_dir:
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
|
|
def log_action(
|
|
self,
|
|
round_num: int,
|
|
platform: str,
|
|
agent_id: int,
|
|
agent_name: str,
|
|
action_type: str,
|
|
action_args: Optional[Dict[str, Any]] = None,
|
|
result: Optional[str] = None,
|
|
success: bool = True
|
|
):
|
|
entry = {
|
|
"round": round_num,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"platform": platform,
|
|
"agent_id": agent_id,
|
|
"agent_name": agent_name,
|
|
"action_type": action_type,
|
|
"action_args": action_args or {},
|
|
"result": result,
|
|
"success": success,
|
|
}
|
|
|
|
with open(self.log_path, 'a', encoding='utf-8') as f:
|
|
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
|
|
|
def log_round_start(self, round_num: int, simulated_hour: int, platform: str):
|
|
entry = {
|
|
"round": round_num,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"platform": platform,
|
|
"event_type": "round_start",
|
|
"simulated_hour": simulated_hour,
|
|
}
|
|
|
|
with open(self.log_path, 'a', encoding='utf-8') as f:
|
|
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
|
|
|
def log_round_end(self, round_num: int, actions_count: int, platform: str):
|
|
entry = {
|
|
"round": round_num,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"platform": platform,
|
|
"event_type": "round_end",
|
|
"actions_count": actions_count,
|
|
}
|
|
|
|
with open(self.log_path, 'a', encoding='utf-8') as f:
|
|
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
|
|
|
def log_simulation_start(self, platform: str, config: Dict[str, Any]):
|
|
entry = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"platform": platform,
|
|
"event_type": "simulation_start",
|
|
"total_rounds": config.get("time_config", {}).get("total_simulation_hours", 72) * 2,
|
|
"agents_count": len(config.get("agent_configs", [])),
|
|
}
|
|
|
|
with open(self.log_path, 'a', encoding='utf-8') as f:
|
|
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
|
|
|
def log_simulation_end(self, platform: str, total_rounds: int, total_actions: int):
|
|
entry = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"platform": platform,
|
|
"event_type": "simulation_end",
|
|
"total_rounds": total_rounds,
|
|
"total_actions": total_actions,
|
|
}
|
|
|
|
with open(self.log_path, 'a', encoding='utf-8') as f:
|
|
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
|
|
|
|
|
# Process-wide logger instance, used by the legacy interface.
|
|
_global_logger: Optional[ActionLogger] = None
|
|
|
|
|
|
def get_logger(log_path: Optional[str] = None) -> ActionLogger:
|
|
"""Return the process-wide :class:`ActionLogger` (legacy interface)."""
|
|
global _global_logger
|
|
|
|
if log_path:
|
|
_global_logger = ActionLogger(log_path)
|
|
|
|
if _global_logger is None:
|
|
_global_logger = ActionLogger("actions.jsonl")
|
|
|
|
return _global_logger
|