docs(i18n): translate chinese docstrings/comments in backend/scripts/{test_profile_format,action_logger}
This commit is contained in:
parent
e1019d91cb
commit
c8c455ceb4
|
|
@ -1,15 +1,17 @@
|
|||
"""
|
||||
动作日志记录器
|
||||
用于记录OASIS模拟中每个Agent的动作,供后端监控使用
|
||||
"""Action logger.
|
||||
|
||||
Records each agent action during an OASIS simulation so the backend can
|
||||
monitor progress.
|
||||
|
||||
Log layout::
|
||||
|
||||
日志结构:
|
||||
sim_xxx/
|
||||
├── twitter/
|
||||
│ └── actions.jsonl # Twitter 平台动作日志
|
||||
│ └── actions.jsonl # Twitter action log
|
||||
├── reddit/
|
||||
│ └── actions.jsonl # Reddit 平台动作日志
|
||||
├── simulation.log # 主模拟进程日志
|
||||
└── run_state.json # 运行状态(API 查询用)
|
||||
│ └── actions.jsonl # Reddit action log
|
||||
├── simulation.log # main simulation process log
|
||||
└── run_state.json # run state (queried by the API)
|
||||
"""
|
||||
|
||||
import json
|
||||
|
|
@ -20,26 +22,25 @@ 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: 平台名称 (twitter/reddit)
|
||||
base_dir: 模拟目录的基础路径
|
||||
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,
|
||||
|
|
@ -50,7 +51,7 @@ class PlatformActionLogger:
|
|||
result: Optional[str] = None,
|
||||
success: bool = True
|
||||
):
|
||||
"""记录一个动作"""
|
||||
"""Append a single action record."""
|
||||
entry = {
|
||||
"round": round_num,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
|
|
@ -61,36 +62,36 @@ class PlatformActionLogger:
|
|||
"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",
|
||||
|
|
@ -98,12 +99,12 @@ class PlatformActionLogger:
|
|||
"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",
|
||||
|
|
@ -111,42 +112,42 @@ class PlatformActionLogger:
|
|||
"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: 模拟目录路径
|
||||
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")
|
||||
|
||||
# 创建 logger
|
||||
|
||||
# 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(
|
||||
|
|
@ -154,8 +155,8 @@ class SimulationLogManager:
|
|||
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(
|
||||
|
|
@ -163,56 +164,56 @@ class SimulationLogManager:
|
|||
datefmt='%H:%M:%S'
|
||||
))
|
||||
self._main_logger.addHandler(console_handler)
|
||||
|
||||
|
||||
self._main_logger.propagate = False
|
||||
|
||||
|
||||
def get_twitter_logger(self) -> PlatformActionLogger:
|
||||
"""获取 Twitter 平台日志记录器"""
|
||||
"""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:
|
||||
"""获取 Reddit 平台日志记录器"""
|
||||
"""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.
|
||||
"""
|
||||
动作日志记录器(兼容旧接口)
|
||||
建议使用 SimulationLogManager 代替
|
||||
"""
|
||||
|
||||
|
||||
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,
|
||||
|
|
@ -235,10 +236,10 @@ class ActionLogger:
|
|||
"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,
|
||||
|
|
@ -247,10 +248,10 @@ class ActionLogger:
|
|||
"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,
|
||||
|
|
@ -259,10 +260,10 @@ class ActionLogger:
|
|||
"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(),
|
||||
|
|
@ -271,10 +272,10 @@ class ActionLogger:
|
|||
"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(),
|
||||
|
|
@ -283,23 +284,23 @@ class ActionLogger:
|
|||
"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
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"""
|
||||
测试Profile格式生成是否符合OASIS要求
|
||||
验证:
|
||||
1. Twitter Profile生成CSV格式
|
||||
2. Reddit Profile生成JSON详细格式
|
||||
"""Profile-format generation tests for OASIS compatibility.
|
||||
|
||||
Verifies that:
|
||||
1. Twitter profiles serialize to CSV format.
|
||||
2. Reddit profiles serialize to detailed JSON format.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -11,19 +11,19 @@ import json
|
|||
import csv
|
||||
import tempfile
|
||||
|
||||
# 添加项目路径
|
||||
# Add the project root to sys.path so the ``app`` package resolves.
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.services.oasis_profile_generator import OasisProfileGenerator, OasisAgentProfile
|
||||
|
||||
|
||||
def test_profile_formats():
|
||||
"""测试Profile格式"""
|
||||
"""Exercise both profile-format outputs end-to-end."""
|
||||
print("=" * 60)
|
||||
print("OASIS Profile格式测试")
|
||||
print("=" * 60)
|
||||
|
||||
# 创建测试Profile数据
|
||||
|
||||
# Build a small set of test profiles.
|
||||
test_profiles = [
|
||||
OasisAgentProfile(
|
||||
user_id=0,
|
||||
|
|
@ -62,18 +62,18 @@ def test_profile_formats():
|
|||
]
|
||||
|
||||
generator = OasisProfileGenerator.__new__(OasisProfileGenerator)
|
||||
|
||||
# 使用临时目录
|
||||
|
||||
# Use a temp directory for the test fixtures.
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
twitter_path = os.path.join(temp_dir, "twitter_profiles.csv")
|
||||
reddit_path = os.path.join(temp_dir, "reddit_profiles.json")
|
||||
|
||||
# 测试Twitter CSV格式
|
||||
|
||||
# Twitter CSV format.
|
||||
print("\n1. 测试Twitter Profile (CSV格式)")
|
||||
print("-" * 40)
|
||||
generator._save_twitter_csv(test_profiles, twitter_path)
|
||||
|
||||
# 读取并验证CSV
|
||||
|
||||
# Read back and verify the CSV.
|
||||
with open(twitter_path, 'r', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
rows = list(reader)
|
||||
|
|
@ -85,8 +85,8 @@ def test_profile_formats():
|
|||
for key, value in rows[0].items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
# 验证必需字段
|
||||
required_twitter_fields = ['user_id', 'user_name', 'name', 'bio',
|
||||
# Verify the required fields are present.
|
||||
required_twitter_fields = ['user_id', 'user_name', 'name', 'bio',
|
||||
'friend_count', 'follower_count', 'statuses_count', 'created_at']
|
||||
missing = set(required_twitter_fields) - set(rows[0].keys())
|
||||
if missing:
|
||||
|
|
@ -94,12 +94,12 @@ def test_profile_formats():
|
|||
else:
|
||||
print(f"\n [通过] 所有必需字段都存在")
|
||||
|
||||
# 测试Reddit JSON格式
|
||||
# Reddit JSON format.
|
||||
print("\n2. 测试Reddit Profile (JSON详细格式)")
|
||||
print("-" * 40)
|
||||
generator._save_reddit_json(test_profiles, reddit_path)
|
||||
|
||||
# 读取并验证JSON
|
||||
|
||||
# Read back and verify the JSON.
|
||||
with open(reddit_path, 'r', encoding='utf-8') as f:
|
||||
reddit_data = json.load(f)
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ def test_profile_formats():
|
|||
print(f"\n 示例数据 (第1条):")
|
||||
print(json.dumps(reddit_data[0], ensure_ascii=False, indent=4))
|
||||
|
||||
# 验证详细格式字段
|
||||
# Verify the detailed Reddit format fields.
|
||||
required_reddit_fields = ['realname', 'username', 'bio', 'persona']
|
||||
optional_reddit_fields = ['age', 'gender', 'mbti', 'country', 'profession', 'interested_topics']
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ def test_profile_formats():
|
|||
|
||||
|
||||
def show_expected_formats():
|
||||
"""显示OASIS期望的格式"""
|
||||
"""Print the canonical OASIS-expected profile formats for reference."""
|
||||
print("\n" + "=" * 60)
|
||||
print("OASIS 期望的Profile格式参考")
|
||||
print("=" * 60)
|
||||
|
|
|
|||
Loading…
Reference in New Issue