666ghj commited on
Commit
72aab2d
·
1 Parent(s): 420da9a

Enhance simulation configuration and management features

Browse files

- Added support for a `max_rounds` parameter in simulation API, allowing users to limit the number of simulation rounds, improving control over simulation duration.
- Updated README.md to reflect the new `max_rounds` parameter and its usage in simulation requests.
- Enhanced error handling for `max_rounds` input validation to ensure it is a positive integer.
- Modified simulation runner and related scripts to incorporate `max_rounds` functionality, ensuring consistent application across Twitter and Reddit simulations.
- Improved logging to indicate when the number of rounds is truncated due to the `max_rounds` setting, enhancing traceability during simulation execution.

backend/README.md CHANGED
@@ -554,7 +554,8 @@ backend/
554
  ```json
555
  {
556
  "simulation_id": "sim_10b494550540",
557
- "platform": "parallel"
 
558
  }
559
  ```
560
 
@@ -562,6 +563,7 @@ backend/
562
  |------|------|------|--------|------|
563
  | simulation_id | String | 是 | - | 模拟ID |
564
  | platform | String | 否 | parallel | 运行平台: twitter/reddit/parallel |
 
565
 
566
  **返回示例**:
567
  ```json
@@ -573,11 +575,15 @@ backend/
573
  "process_pid": 12345,
574
  "twitter_running": true,
575
  "reddit_running": true,
576
- "started_at": "2025-12-02T11:00:00"
 
 
577
  }
578
  }
579
  ```
580
 
 
 
581
  ---
582
 
583
  #### 5. 停止模拟
@@ -1502,12 +1508,13 @@ curl -X POST http://localhost:5001/api/simulation/prepare/status \
1502
 
1503
  # 等待status=completed
1504
 
1505
- # Step 7: 启动模拟
1506
  curl -X POST http://localhost:5001/api/simulation/start \
1507
  -H "Content-Type: application/json" \
1508
  -d '{
1509
  "simulation_id": "sim_xxx",
1510
- "platform": "parallel"
 
1511
  }'
1512
 
1513
  # Step 8: 实时查询运行状态
 
554
  ```json
555
  {
556
  "simulation_id": "sim_10b494550540",
557
+ "platform": "parallel",
558
+ "max_rounds": 100
559
  }
560
  ```
561
 
 
563
  |------|------|------|--------|------|
564
  | simulation_id | String | 是 | - | 模拟ID |
565
  | platform | String | 否 | parallel | 运行平台: twitter/reddit/parallel |
566
+ | max_rounds | Integer | 否 | - | 最大模拟轮数,用于截断过长的模拟。如果配置中的轮数超过此值,将被截断 |
567
 
568
  **返回示例**:
569
  ```json
 
575
  "process_pid": 12345,
576
  "twitter_running": true,
577
  "reddit_running": true,
578
+ "started_at": "2025-12-02T11:00:00",
579
+ "total_rounds": 100,
580
+ "max_rounds_applied": 100
581
  }
582
  }
583
  ```
584
 
585
+ > **说明**: `max_rounds_applied` 字段仅在指定了 `max_rounds` 参数时返回,表示实际应用的最大轮数限制。
586
+
587
  ---
588
 
589
  #### 5. 停止模拟
 
1508
 
1509
  # 等待status=completed
1510
 
1511
+ # Step 7: 启动模拟(可选指定max_rounds限制轮数)
1512
  curl -X POST http://localhost:5001/api/simulation/start \
1513
  -H "Content-Type: application/json" \
1514
  -d '{
1515
  "simulation_id": "sim_xxx",
1516
+ "platform": "parallel",
1517
+ "max_rounds": 50
1518
  }'
1519
 
1520
  # Step 8: 实时查询运行状态
backend/app/__init__.py CHANGED
@@ -15,6 +15,11 @@ def create_app(config_class=Config):
15
  app = Flask(__name__)
16
  app.config.from_object(config_class)
17
 
 
 
 
 
 
18
  # 设置日志
19
  logger = setup_logger('mirofish')
20
 
 
15
  app = Flask(__name__)
16
  app.config.from_object(config_class)
17
 
18
+ # 设置JSON编码:确保中文直接显示(而不是 \uXXXX 格式)
19
+ # Flask >= 2.3 使用 app.json.ensure_ascii,旧版本使用 JSON_AS_ASCII 配置
20
+ if hasattr(app, 'json') and hasattr(app.json, 'ensure_ascii'):
21
+ app.json.ensure_ascii = False
22
+
23
  # 设置日志
24
  logger = setup_logger('mirofish')
25
 
backend/app/api/simulation.py CHANGED
@@ -1114,7 +1114,8 @@ def start_simulation():
1114
  请求(JSON):
1115
  {
1116
  "simulation_id": "sim_xxxx", // 必填,模拟ID
1117
- "platform": "parallel" // 可选: twitter / reddit / parallel (默认)
 
1118
  }
1119
 
1120
  返回:
@@ -1141,6 +1142,22 @@ def start_simulation():
1141
  }), 400
1142
 
1143
  platform = data.get('platform', 'parallel')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1144
 
1145
  if platform not in ['twitter', 'reddit', 'parallel']:
1146
  return jsonify({
@@ -1187,15 +1204,19 @@ def start_simulation():
1187
  }), 400
1188
 
1189
  # 启动模拟
1190
- run_state = SimulationRunner.start_simulation(simulation_id, platform)
1191
 
1192
  # 更新模拟状态
1193
  state.status = SimulationStatus.RUNNING
1194
  manager._save_simulation_state(state)
1195
 
 
 
 
 
1196
  return jsonify({
1197
  "success": True,
1198
- "data": run_state.to_dict()
1199
  })
1200
 
1201
  except ValueError as e:
 
1114
  请求(JSON):
1115
  {
1116
  "simulation_id": "sim_xxxx", // 必填,模拟ID
1117
+ "platform": "parallel", // 可选: twitter / reddit / parallel (默认)
1118
+ "max_rounds": 100 // 可选: 最大模拟轮数,用于截断过长的模拟
1119
  }
1120
 
1121
  返回:
 
1142
  }), 400
1143
 
1144
  platform = data.get('platform', 'parallel')
1145
+ max_rounds = data.get('max_rounds') # 可选:最大模拟轮数
1146
+
1147
+ # 验证 max_rounds 参数
1148
+ if max_rounds is not None:
1149
+ try:
1150
+ max_rounds = int(max_rounds)
1151
+ if max_rounds <= 0:
1152
+ return jsonify({
1153
+ "success": False,
1154
+ "error": "max_rounds 必须是正整数"
1155
+ }), 400
1156
+ except (ValueError, TypeError):
1157
+ return jsonify({
1158
+ "success": False,
1159
+ "error": "max_rounds 必须是有效的整数"
1160
+ }), 400
1161
 
1162
  if platform not in ['twitter', 'reddit', 'parallel']:
1163
  return jsonify({
 
1204
  }), 400
1205
 
1206
  # 启动模拟
1207
+ run_state = SimulationRunner.start_simulation(simulation_id, platform, max_rounds)
1208
 
1209
  # 更新模拟状态
1210
  state.status = SimulationStatus.RUNNING
1211
  manager._save_simulation_state(state)
1212
 
1213
+ response_data = run_state.to_dict()
1214
+ if max_rounds:
1215
+ response_data['max_rounds_applied'] = max_rounds
1216
+
1217
  return jsonify({
1218
  "success": True,
1219
+ "data": response_data
1220
  })
1221
 
1222
  except ValueError as e:
backend/app/config.py CHANGED
@@ -24,6 +24,9 @@ class Config:
24
  SECRET_KEY = os.environ.get('SECRET_KEY', 'mirofish-secret-key')
25
  DEBUG = os.environ.get('FLASK_DEBUG', 'True').lower() == 'true'
26
 
 
 
 
27
  # LLM配置(统一使用OpenAI格式)
28
  LLM_API_KEY = os.environ.get('LLM_API_KEY')
29
  LLM_BASE_URL = os.environ.get('LLM_BASE_URL', 'https://api.openai.com/v1')
 
24
  SECRET_KEY = os.environ.get('SECRET_KEY', 'mirofish-secret-key')
25
  DEBUG = os.environ.get('FLASK_DEBUG', 'True').lower() == 'true'
26
 
27
+ # JSON配置 - 禁用ASCII转义,让中文直接显示(而不是 \uXXXX 格式)
28
+ JSON_AS_ASCII = False
29
+
30
  # LLM配置(统一使用OpenAI格式)
31
  LLM_API_KEY = os.environ.get('LLM_API_KEY')
32
  LLM_BASE_URL = os.environ.get('LLM_BASE_URL', 'https://api.openai.com/v1')
backend/app/services/simulation_config_generator.py CHANGED
@@ -85,8 +85,8 @@ class TimeSimulationConfig:
85
  # 模拟总时长(模拟小时数)
86
  total_simulation_hours: int = 72 # 默认模拟72小时(3天)
87
 
88
- # 每轮代表的时间(模拟分钟)
89
- minutes_per_round: int = 30
90
 
91
  # 每小时激活的Agent数量范围
92
  agents_per_hour_min: int = 5
@@ -205,7 +205,7 @@ class SimulationConfigGenerator:
205
 
206
  采用分步生成策略:
207
  1. 生成时间配置和事件配置(轻量级)
208
- 2. 分批生成Agent配置(每批10-15个)
209
  3. 生成平台配置
210
  """
211
 
@@ -214,6 +214,13 @@ class SimulationConfigGenerator:
214
  # 每批生成的Agent数量
215
  AGENTS_PER_BATCH = 15
216
 
 
 
 
 
 
 
 
217
  def __init__(
218
  self,
219
  api_key: Optional[str] = None,
@@ -286,8 +293,9 @@ class SimulationConfigGenerator:
286
 
287
  # ========== 步骤1: 生成时间配置 ==========
288
  report_progress(1, "生成时间配置...")
289
- time_config_result = self._generate_time_config(context, len(entities))
290
- time_config = self._parse_time_config(time_config_result)
 
291
  reasoning_parts.append(f"时间配置: {time_config_result.get('reasoning', '成功')}")
292
 
293
  # ========== 步骤2: 生成事件配置 ==========
@@ -411,11 +419,14 @@ class SimulationConfigGenerator:
411
 
412
  for entity_type, type_entities in by_type.items():
413
  lines.append(f"\n### {entity_type} ({len(type_entities)}个)")
414
- for e in type_entities[:10]: # 每类最多显示10个
415
- summary_preview = (e.summary[:100] + "...") if len(e.summary) > 100 else e.summary
 
 
 
416
  lines.append(f"- {e.name}: {summary_preview}")
417
- if len(type_entities) > 10:
418
- lines.append(f" ... 还有 {len(type_entities) - 10} 个")
419
 
420
  return "\n".join(lines)
421
 
@@ -522,33 +533,56 @@ class SimulationConfigGenerator:
522
 
523
  def _generate_time_config(self, context: str, num_entities: int) -> Dict[str, Any]:
524
  """生成时间配置"""
 
 
 
 
 
 
525
  prompt = f"""基于以下模拟需求,生成时间模拟配置。
526
 
527
- {context[:5000]}
528
 
529
  ## 任务
530
- 请生成时间配置JSON,注意:
 
 
531
  - 用户群体为中国人,需符合北京时间作息习惯
532
  - 凌晨0-5点几乎无人活动(活跃度系数0.05)
533
  - 早上6-8点逐渐活跃(活跃度系数0.4)
534
  - 工作时间9-18点中等活跃(活跃度系数0.7)
535
  - 晚间19-22点是高峰期(活跃度系数1.5)
536
  - 23点后活跃度下降(活跃度系数0.5)
 
 
 
 
537
 
538
- 当前实体数量: {num_entities}
539
 
540
- 返回JSON格式(不要markdown)
541
  {{
542
- "total_simulation_hours": <72-168,根据事件性质决定>,
543
- "minutes_per_round": <15-60>,
544
- "agents_per_hour_min": <每小时最少激活Agent数>,
545
- "agents_per_hour_max": <每小时最多激活Agent数>,
546
  "peak_hours": [19, 20, 21, 22],
547
  "off_peak_hours": [0, 1, 2, 3, 4, 5],
548
  "morning_hours": [6, 7, 8],
549
  "work_hours": [9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
550
- "reasoning": "<简要说明>"
551
- }}"""
 
 
 
 
 
 
 
 
 
 
 
552
 
553
  system_prompt = "你是社交媒体模拟专家。返回纯JSON格式,时间配置需符合中国人作息习惯。"
554
 
@@ -562,23 +596,41 @@ class SimulationConfigGenerator:
562
  """获取默认时间配置(中国人作息)"""
563
  return {
564
  "total_simulation_hours": 72,
565
- "minutes_per_round": 30,
566
  "agents_per_hour_min": max(1, num_entities // 15),
567
  "agents_per_hour_max": max(5, num_entities // 5),
568
  "peak_hours": [19, 20, 21, 22],
569
  "off_peak_hours": [0, 1, 2, 3, 4, 5],
570
  "morning_hours": [6, 7, 8],
571
  "work_hours": [9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
572
- "reasoning": "使用默认中国人作息配置"
573
  }
574
 
575
- def _parse_time_config(self, result: Dict[str, Any]) -> TimeSimulationConfig:
576
- """解析时间配置结果"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
577
  return TimeSimulationConfig(
578
  total_simulation_hours=result.get("total_simulation_hours", 72),
579
- minutes_per_round=result.get("minutes_per_round", 30),
580
- agents_per_hour_min=result.get("agents_per_hour_min", 5),
581
- agents_per_hour_max=result.get("agents_per_hour_max", 20),
582
  peak_hours=result.get("peak_hours", [19, 20, 21, 22]),
583
  off_peak_hours=result.get("off_peak_hours", [0, 1, 2, 3, 4, 5]),
584
  off_peak_activity_multiplier=0.05, # 凌晨几乎无人
@@ -616,11 +668,14 @@ class SimulationConfigGenerator:
616
  for t, examples in type_examples.items()
617
  ])
618
 
 
 
 
619
  prompt = f"""基于以下模拟需求,生成事件配置。
620
 
621
  模拟需求: {simulation_requirement}
622
 
623
- {context[:3000]}
624
 
625
  ## 可用实体类型及示例
626
  {type_info}
@@ -761,14 +816,15 @@ class SimulationConfigGenerator:
761
  ) -> List[AgentActivityConfig]:
762
  """分批生成Agent配置"""
763
 
764
- # 构建实体信息
765
  entity_list = []
 
766
  for i, e in enumerate(entities):
767
  entity_list.append({
768
  "agent_id": start_idx + i,
769
  "entity_name": e.name,
770
  "entity_type": e.get_entity_type() or "Unknown",
771
- "summary": e.summary[:150] if e.summary else ""
772
  })
773
 
774
  prompt = f"""基于以下信息,为每个实体生成社交媒体活动配置。
 
85
  # 模拟总时长(模拟小时数)
86
  total_simulation_hours: int = 72 # 默认模拟72小时(3天)
87
 
88
+ # 每轮代表的时间(模拟分钟)- 默认60分钟(1小时),加快时间流速
89
+ minutes_per_round: int = 60
90
 
91
  # 每小时激活的Agent数量范围
92
  agents_per_hour_min: int = 5
 
205
 
206
  采用分步生成策略:
207
  1. 生成时间配置和事件配置(轻量级)
208
+ 2. 分批生成Agent配置(每批10-20个)
209
  3. 生成平台配置
210
  """
211
 
 
214
  # 每批生成的Agent数量
215
  AGENTS_PER_BATCH = 15
216
 
217
+ # 各步骤的上下文截断长度(字符数)
218
+ TIME_CONFIG_CONTEXT_LENGTH = 10000 # 时间配置
219
+ EVENT_CONFIG_CONTEXT_LENGTH = 8000 # 事件配置
220
+ ENTITY_SUMMARY_LENGTH = 300 # 实体摘要
221
+ AGENT_SUMMARY_LENGTH = 300 # Agent配置中的实体摘要
222
+ ENTITIES_PER_TYPE_DISPLAY = 20 # 每类实体显示数量
223
+
224
  def __init__(
225
  self,
226
  api_key: Optional[str] = None,
 
293
 
294
  # ========== 步骤1: 生成时间配置 ==========
295
  report_progress(1, "生成时间配置...")
296
+ num_entities = len(entities)
297
+ time_config_result = self._generate_time_config(context, num_entities)
298
+ time_config = self._parse_time_config(time_config_result, num_entities)
299
  reasoning_parts.append(f"时间配置: {time_config_result.get('reasoning', '成功')}")
300
 
301
  # ========== 步骤2: 生成事件配置 ==========
 
419
 
420
  for entity_type, type_entities in by_type.items():
421
  lines.append(f"\n### {entity_type} ({len(type_entities)}个)")
422
+ # 使用配置的显示数量和摘要长度
423
+ display_count = self.ENTITIES_PER_TYPE_DISPLAY
424
+ summary_len = self.ENTITY_SUMMARY_LENGTH
425
+ for e in type_entities[:display_count]:
426
+ summary_preview = (e.summary[:summary_len] + "...") if len(e.summary) > summary_len else e.summary
427
  lines.append(f"- {e.name}: {summary_preview}")
428
+ if len(type_entities) > display_count:
429
+ lines.append(f" ... 还有 {len(type_entities) - display_count} 个")
430
 
431
  return "\n".join(lines)
432
 
 
533
 
534
  def _generate_time_config(self, context: str, num_entities: int) -> Dict[str, Any]:
535
  """生成时间配置"""
536
+ # 使用配置的上下文截断长度
537
+ context_truncated = context[:self.TIME_CONFIG_CONTEXT_LENGTH]
538
+
539
+ # 计算最大允许值(80%的agent数)
540
+ max_agents_allowed = max(1, int(num_entities * 0.9))
541
+
542
  prompt = f"""基于以下模拟需求,生成时间模拟配置。
543
 
544
+ {context_truncated}
545
 
546
  ## 任务
547
+ 请生成时间配置JSON
548
+
549
+ ### 基本原则(仅供参考,需根据具体事件和参与群体灵活调整):
550
  - 用户群体为中国人,需符合北京时间作息习惯
551
  - 凌晨0-5点几乎无人活动(活跃度系数0.05)
552
  - 早上6-8点逐渐活跃(活跃度系数0.4)
553
  - 工作时间9-18点中等活跃(活跃度系数0.7)
554
  - 晚间19-22点是高峰期(活跃度系数1.5)
555
  - 23点后活跃度下降(活跃度系数0.5)
556
+ - 一般规律:凌晨低活跃、早间渐增、工作时段中等、晚间高峰
557
+ - **重要**:以下示例值仅供参考,你需要根据事件性质、参与群体特点来调整具体时段
558
+ - 例如:学生群体高峰可能是21-23点;媒体全天活跃;官方机构只在工作时间
559
+ - 例如:突发热点可能导致深夜也有讨论,off_peak_hours 可适当缩短
560
 
561
+ ### 返回JSON格式(不要markdown)
562
 
563
+ 示例
564
  {{
565
+ "total_simulation_hours": 72,
566
+ "minutes_per_round": 60,
567
+ "agents_per_hour_min": 5,
568
+ "agents_per_hour_max": 50,
569
  "peak_hours": [19, 20, 21, 22],
570
  "off_peak_hours": [0, 1, 2, 3, 4, 5],
571
  "morning_hours": [6, 7, 8],
572
  "work_hours": [9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
573
+ "reasoning": "针对该事件的时间配置说明"
574
+ }}
575
+
576
+ 字段说明:
577
+ - total_simulation_hours (int): 模拟总时长,24-168小时,突发事件短、持续话题长
578
+ - minutes_per_round (int): 每轮时长,30-120分钟,建议60分钟
579
+ - agents_per_hour_min (int): 每小时最少激活Agent数(取值范围: 1-{max_agents_allowed})
580
+ - agents_per_hour_max (int): 每小时最多激活Agent数(取值范围: 1-{max_agents_allowed})
581
+ - peak_hours (int数组): 高峰时段,根据事件参与群体调整
582
+ - off_peak_hours (int数组): 低谷时段,通常深夜凌晨
583
+ - morning_hours (int数组): 早间时段
584
+ - work_hours (int数组): 工作时段
585
+ - reasoning (string): 简要说明为什么这样配置"""
586
 
587
  system_prompt = "你是社交媒体模拟专家。返回纯JSON格式,时间配置需符合中国人作息习惯。"
588
 
 
596
  """获取默认时间配置(中国人作息)"""
597
  return {
598
  "total_simulation_hours": 72,
599
+ "minutes_per_round": 60, # 每轮1小时,加快时间流速
600
  "agents_per_hour_min": max(1, num_entities // 15),
601
  "agents_per_hour_max": max(5, num_entities // 5),
602
  "peak_hours": [19, 20, 21, 22],
603
  "off_peak_hours": [0, 1, 2, 3, 4, 5],
604
  "morning_hours": [6, 7, 8],
605
  "work_hours": [9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
606
+ "reasoning": "使用默认中国人作息配置(每轮1小时)"
607
  }
608
 
609
+ def _parse_time_config(self, result: Dict[str, Any], num_entities: int) -> TimeSimulationConfig:
610
+ """解析时间配置结果,并验证agents_per_hour值不超过总agent数"""
611
+ # 获取原始值
612
+ agents_per_hour_min = result.get("agents_per_hour_min", max(1, num_entities // 15))
613
+ agents_per_hour_max = result.get("agents_per_hour_max", max(5, num_entities // 5))
614
+
615
+ # 验证并修正:确保不超过总agent数
616
+ if agents_per_hour_min > num_entities:
617
+ logger.warning(f"agents_per_hour_min ({agents_per_hour_min}) 超过总Agent数 ({num_entities}),已修正")
618
+ agents_per_hour_min = max(1, num_entities // 10)
619
+
620
+ if agents_per_hour_max > num_entities:
621
+ logger.warning(f"agents_per_hour_max ({agents_per_hour_max}) 超过总Agent数 ({num_entities}),已修正")
622
+ agents_per_hour_max = max(agents_per_hour_min + 1, num_entities // 2)
623
+
624
+ # 确保 min < max
625
+ if agents_per_hour_min >= agents_per_hour_max:
626
+ agents_per_hour_min = max(1, agents_per_hour_max // 2)
627
+ logger.warning(f"agents_per_hour_min >= max,已修正为 {agents_per_hour_min}")
628
+
629
  return TimeSimulationConfig(
630
  total_simulation_hours=result.get("total_simulation_hours", 72),
631
+ minutes_per_round=result.get("minutes_per_round", 60), # 默认每轮1小时
632
+ agents_per_hour_min=agents_per_hour_min,
633
+ agents_per_hour_max=agents_per_hour_max,
634
  peak_hours=result.get("peak_hours", [19, 20, 21, 22]),
635
  off_peak_hours=result.get("off_peak_hours", [0, 1, 2, 3, 4, 5]),
636
  off_peak_activity_multiplier=0.05, # 凌晨几乎无人
 
668
  for t, examples in type_examples.items()
669
  ])
670
 
671
+ # 使用配置的上下文截断长度
672
+ context_truncated = context[:self.EVENT_CONFIG_CONTEXT_LENGTH]
673
+
674
  prompt = f"""基于以下模拟需求,生成事件配置。
675
 
676
  模拟需求: {simulation_requirement}
677
 
678
+ {context_truncated}
679
 
680
  ## 可用实体类型及示例
681
  {type_info}
 
816
  ) -> List[AgentActivityConfig]:
817
  """分批生成Agent配置"""
818
 
819
+ # 构建实体信息(使用配置的摘要长度)
820
  entity_list = []
821
+ summary_len = self.AGENT_SUMMARY_LENGTH
822
  for i, e in enumerate(entities):
823
  entity_list.append({
824
  "agent_id": start_idx + i,
825
  "entity_name": e.name,
826
  "entity_type": e.get_entity_type() or "Unknown",
827
+ "summary": e.summary[:summary_len] if e.summary else ""
828
  })
829
 
830
  prompt = f"""基于以下信息,为每个实体生成社交媒体活动配置。
backend/app/services/simulation_runner.py CHANGED
@@ -280,7 +280,8 @@ class SimulationRunner:
280
  def start_simulation(
281
  cls,
282
  simulation_id: str,
283
- platform: str = "parallel" # twitter / reddit / parallel
 
284
  ) -> SimulationRunState:
285
  """
286
  启动模拟
@@ -288,6 +289,7 @@ class SimulationRunner:
288
  Args:
289
  simulation_id: 模拟ID
290
  platform: 运行平台 (twitter/reddit/parallel)
 
291
 
292
  Returns:
293
  SimulationRunState
@@ -313,6 +315,13 @@ class SimulationRunner:
313
  minutes_per_round = time_config.get("minutes_per_round", 30)
314
  total_rounds = int(total_hours * 60 / minutes_per_round)
315
 
 
 
 
 
 
 
 
316
  state = SimulationRunState(
317
  simulation_id=simulation_id,
318
  runner_status=RunnerStatus.STARTING,
@@ -358,6 +367,10 @@ class SimulationRunner:
358
  "--config", config_path, # 使用完整配置文件路径
359
  ]
360
 
 
 
 
 
361
  # 创建主日志文件,避免 stdout/stderr 管道缓冲区满导致进程阻塞
362
  main_log_path = os.path.join(sim_dir, "simulation.log")
363
  main_log_file = open(main_log_path, 'w', encoding='utf-8')
 
280
  def start_simulation(
281
  cls,
282
  simulation_id: str,
283
+ platform: str = "parallel", # twitter / reddit / parallel
284
+ max_rounds: int = None # 最大模拟轮数(可选,用于截断过长的模拟)
285
  ) -> SimulationRunState:
286
  """
287
  启动模拟
 
289
  Args:
290
  simulation_id: 模拟ID
291
  platform: 运行平台 (twitter/reddit/parallel)
292
+ max_rounds: 最大模拟轮数(可选,用于截断过长的模拟)
293
 
294
  Returns:
295
  SimulationRunState
 
315
  minutes_per_round = time_config.get("minutes_per_round", 30)
316
  total_rounds = int(total_hours * 60 / minutes_per_round)
317
 
318
+ # 如果指定了最大轮数,则截断
319
+ if max_rounds is not None and max_rounds > 0:
320
+ original_rounds = total_rounds
321
+ total_rounds = min(total_rounds, max_rounds)
322
+ if total_rounds < original_rounds:
323
+ logger.info(f"轮数已截断: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})")
324
+
325
  state = SimulationRunState(
326
  simulation_id=simulation_id,
327
  runner_status=RunnerStatus.STARTING,
 
367
  "--config", config_path, # 使用完整配置文件路径
368
  ]
369
 
370
+ # 如果指定了最大轮数,添加到命令行参数
371
+ if max_rounds is not None and max_rounds > 0:
372
+ cmd.extend(["--max-rounds", str(max_rounds)])
373
+
374
  # 创建主日志文件,避免 stdout/stderr 管道缓冲区满导致进程阻塞
375
  main_log_path = os.path.join(sim_dir, "simulation.log")
376
  main_log_file = open(main_log_path, 'w', encoding='utf-8')
backend/scripts/run_parallel_simulation.py CHANGED
@@ -404,9 +404,18 @@ async def run_twitter_simulation(
404
  config: Dict[str, Any],
405
  simulation_dir: str,
406
  action_logger: Optional[PlatformActionLogger] = None,
407
- main_logger: Optional[SimulationLogManager] = None
 
408
  ):
409
- """运行Twitter模拟"""
 
 
 
 
 
 
 
 
410
  def log_info(msg):
411
  if main_logger:
412
  main_logger.info(f"[Twitter] {msg}")
@@ -494,6 +503,13 @@ async def run_twitter_simulation(
494
  minutes_per_round = time_config.get("minutes_per_round", 30)
495
  total_rounds = (total_hours * 60) // minutes_per_round
496
 
 
 
 
 
 
 
 
497
  start_time = datetime.now()
498
 
499
  for round_num in range(total_rounds):
@@ -552,9 +568,18 @@ async def run_reddit_simulation(
552
  config: Dict[str, Any],
553
  simulation_dir: str,
554
  action_logger: Optional[PlatformActionLogger] = None,
555
- main_logger: Optional[SimulationLogManager] = None
 
556
  ):
557
- """运行Reddit模拟"""
 
 
 
 
 
 
 
 
558
  def log_info(msg):
559
  if main_logger:
560
  main_logger.info(f"[Reddit] {msg}")
@@ -649,6 +674,13 @@ async def run_reddit_simulation(
649
  minutes_per_round = time_config.get("minutes_per_round", 30)
650
  total_rounds = (total_hours * 60) // minutes_per_round
651
 
 
 
 
 
 
 
 
652
  start_time = datetime.now()
653
 
654
  for round_num in range(total_rounds):
@@ -721,6 +753,12 @@ async def main():
721
  action='store_true',
722
  help='只运行Reddit模拟'
723
  )
 
 
 
 
 
 
724
 
725
  args = parser.parse_args()
726
 
@@ -746,9 +784,18 @@ async def main():
746
  log_manager.info("=" * 60)
747
 
748
  time_config = config.get("time_config", {})
 
 
 
 
749
  log_manager.info(f"模拟参数:")
750
- log_manager.info(f" - 总模拟时长: {time_config.get('total_simulation_hours', 72)}小时")
751
- log_manager.info(f" - 每轮时间: {time_config.get('minutes_per_round', 30)}分钟")
 
 
 
 
 
752
  log_manager.info(f" - Agent数量: {len(config.get('agent_configs', []))}")
753
 
754
  log_manager.info("日志结构:")
@@ -760,14 +807,14 @@ async def main():
760
  start_time = datetime.now()
761
 
762
  if args.twitter_only:
763
- await run_twitter_simulation(config, simulation_dir, twitter_logger, log_manager)
764
  elif args.reddit_only:
765
- await run_reddit_simulation(config, simulation_dir, reddit_logger, log_manager)
766
  else:
767
  # 并行运行(每个平台使用独立的日志记录器)
768
  await asyncio.gather(
769
- run_twitter_simulation(config, simulation_dir, twitter_logger, log_manager),
770
- run_reddit_simulation(config, simulation_dir, reddit_logger, log_manager),
771
  )
772
 
773
  total_elapsed = (datetime.now() - start_time).total_seconds()
 
404
  config: Dict[str, Any],
405
  simulation_dir: str,
406
  action_logger: Optional[PlatformActionLogger] = None,
407
+ main_logger: Optional[SimulationLogManager] = None,
408
+ max_rounds: Optional[int] = None
409
  ):
410
+ """运行Twitter模拟
411
+
412
+ Args:
413
+ config: 模拟配置
414
+ simulation_dir: 模拟目录
415
+ action_logger: 动作日志记录器
416
+ main_logger: 主日志管理器
417
+ max_rounds: 最大模拟轮数(可选,用于截断过长的模拟)
418
+ """
419
  def log_info(msg):
420
  if main_logger:
421
  main_logger.info(f"[Twitter] {msg}")
 
503
  minutes_per_round = time_config.get("minutes_per_round", 30)
504
  total_rounds = (total_hours * 60) // minutes_per_round
505
 
506
+ # 如果指定了最大轮数,则截断
507
+ if max_rounds is not None and max_rounds > 0:
508
+ original_rounds = total_rounds
509
+ total_rounds = min(total_rounds, max_rounds)
510
+ if total_rounds < original_rounds:
511
+ log_info(f"轮数已截断: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})")
512
+
513
  start_time = datetime.now()
514
 
515
  for round_num in range(total_rounds):
 
568
  config: Dict[str, Any],
569
  simulation_dir: str,
570
  action_logger: Optional[PlatformActionLogger] = None,
571
+ main_logger: Optional[SimulationLogManager] = None,
572
+ max_rounds: Optional[int] = None
573
  ):
574
+ """运行Reddit模拟
575
+
576
+ Args:
577
+ config: 模拟配置
578
+ simulation_dir: 模拟目录
579
+ action_logger: 动作日志记录器
580
+ main_logger: 主日志管理器
581
+ max_rounds: 最大模拟轮数(可选,用于截断过长的模拟)
582
+ """
583
  def log_info(msg):
584
  if main_logger:
585
  main_logger.info(f"[Reddit] {msg}")
 
674
  minutes_per_round = time_config.get("minutes_per_round", 30)
675
  total_rounds = (total_hours * 60) // minutes_per_round
676
 
677
+ # 如果指定了最大轮数,则截断
678
+ if max_rounds is not None and max_rounds > 0:
679
+ original_rounds = total_rounds
680
+ total_rounds = min(total_rounds, max_rounds)
681
+ if total_rounds < original_rounds:
682
+ log_info(f"轮数已截断: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})")
683
+
684
  start_time = datetime.now()
685
 
686
  for round_num in range(total_rounds):
 
753
  action='store_true',
754
  help='只运行Reddit模拟'
755
  )
756
+ parser.add_argument(
757
+ '--max-rounds',
758
+ type=int,
759
+ default=None,
760
+ help='最大模拟轮数(可选,用于截断过长的模拟)'
761
+ )
762
 
763
  args = parser.parse_args()
764
 
 
784
  log_manager.info("=" * 60)
785
 
786
  time_config = config.get("time_config", {})
787
+ total_hours = time_config.get('total_simulation_hours', 72)
788
+ minutes_per_round = time_config.get('minutes_per_round', 30)
789
+ config_total_rounds = (total_hours * 60) // minutes_per_round
790
+
791
  log_manager.info(f"模拟参数:")
792
+ log_manager.info(f" - 总模拟时长: {total_hours}小时")
793
+ log_manager.info(f" - 每轮时间: {minutes_per_round}分钟")
794
+ log_manager.info(f" - 配置总轮数: {config_total_rounds}")
795
+ if args.max_rounds:
796
+ log_manager.info(f" - 最大轮数限制: {args.max_rounds}")
797
+ if args.max_rounds < config_total_rounds:
798
+ log_manager.info(f" - 实际执行轮数: {args.max_rounds} (已截断)")
799
  log_manager.info(f" - Agent数量: {len(config.get('agent_configs', []))}")
800
 
801
  log_manager.info("日志结构:")
 
807
  start_time = datetime.now()
808
 
809
  if args.twitter_only:
810
+ await run_twitter_simulation(config, simulation_dir, twitter_logger, log_manager, args.max_rounds)
811
  elif args.reddit_only:
812
+ await run_reddit_simulation(config, simulation_dir, reddit_logger, log_manager, args.max_rounds)
813
  else:
814
  # 并行运行(每个平台使用独立的日志记录器)
815
  await asyncio.gather(
816
+ run_twitter_simulation(config, simulation_dir, twitter_logger, log_manager, args.max_rounds),
817
+ run_reddit_simulation(config, simulation_dir, reddit_logger, log_manager, args.max_rounds),
818
  )
819
 
820
  total_elapsed = (datetime.now() - start_time).total_seconds()
backend/scripts/run_reddit_simulation.py CHANGED
@@ -251,8 +251,12 @@ class RedditSimulationRunner:
251
 
252
  return active_agents
253
 
254
- async def run(self):
255
- """运行Reddit模拟"""
 
 
 
 
256
  print("=" * 60)
257
  print("OASIS Reddit模拟")
258
  print(f"配置文件: {self.config_path}")
@@ -264,10 +268,19 @@ class RedditSimulationRunner:
264
  minutes_per_round = time_config.get("minutes_per_round", 30)
265
  total_rounds = (total_hours * 60) // minutes_per_round
266
 
 
 
 
 
 
 
 
267
  print(f"\n模拟参数:")
268
  print(f" - 总模拟时长: {total_hours}小时")
269
  print(f" - 每轮时间: {minutes_per_round}分钟")
270
  print(f" - 总轮数: {total_rounds}")
 
 
271
  print(f" - Agent数量: {len(self.config.get('agent_configs', []))}")
272
 
273
  print("\n初始化LLM模型...")
@@ -380,6 +393,12 @@ async def main():
380
  required=True,
381
  help='配置文件路径 (simulation_config.json)'
382
  )
 
 
 
 
 
 
383
 
384
  args = parser.parse_args()
385
 
@@ -392,7 +411,7 @@ async def main():
392
  setup_oasis_logging(os.path.join(simulation_dir, "log"))
393
 
394
  runner = RedditSimulationRunner(args.config)
395
- await runner.run()
396
 
397
 
398
  if __name__ == "__main__":
 
251
 
252
  return active_agents
253
 
254
+ async def run(self, max_rounds: int = None):
255
+ """运行Reddit模拟
256
+
257
+ Args:
258
+ max_rounds: 最大模拟轮数(可选,用于截断过长的模拟)
259
+ """
260
  print("=" * 60)
261
  print("OASIS Reddit模拟")
262
  print(f"配置文件: {self.config_path}")
 
268
  minutes_per_round = time_config.get("minutes_per_round", 30)
269
  total_rounds = (total_hours * 60) // minutes_per_round
270
 
271
+ # 如果指定了最大轮数,则截断
272
+ if max_rounds is not None and max_rounds > 0:
273
+ original_rounds = total_rounds
274
+ total_rounds = min(total_rounds, max_rounds)
275
+ if total_rounds < original_rounds:
276
+ print(f"\n轮数已截断: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})")
277
+
278
  print(f"\n模拟参数:")
279
  print(f" - 总模拟时长: {total_hours}小时")
280
  print(f" - 每轮时间: {minutes_per_round}分钟")
281
  print(f" - 总轮数: {total_rounds}")
282
+ if max_rounds:
283
+ print(f" - 最大轮数限制: {max_rounds}")
284
  print(f" - Agent数量: {len(self.config.get('agent_configs', []))}")
285
 
286
  print("\n初始化LLM模型...")
 
393
  required=True,
394
  help='配置文件路径 (simulation_config.json)'
395
  )
396
+ parser.add_argument(
397
+ '--max-rounds',
398
+ type=int,
399
+ default=None,
400
+ help='最大模拟轮数(可选,用于截断过长的模拟)'
401
+ )
402
 
403
  args = parser.parse_args()
404
 
 
411
  setup_oasis_logging(os.path.join(simulation_dir, "log"))
412
 
413
  runner = RedditSimulationRunner(args.config)
414
+ await runner.run(max_rounds=args.max_rounds)
415
 
416
 
417
  if __name__ == "__main__":
backend/scripts/run_twitter_simulation.py CHANGED
@@ -259,8 +259,12 @@ class TwitterSimulationRunner:
259
 
260
  return active_agents
261
 
262
- async def run(self):
263
- """运行Twitter模拟"""
 
 
 
 
264
  print("=" * 60)
265
  print("OASIS Twitter模拟")
266
  print(f"配置文件: {self.config_path}")
@@ -275,10 +279,19 @@ class TwitterSimulationRunner:
275
  # 计算总轮数
276
  total_rounds = (total_hours * 60) // minutes_per_round
277
 
 
 
 
 
 
 
 
278
  print(f"\n模拟参数:")
279
  print(f" - 总模拟时长: {total_hours}小时")
280
  print(f" - 每轮时间: {minutes_per_round}分钟")
281
  print(f" - 总轮数: {total_rounds}")
 
 
282
  print(f" - Agent数量: {len(self.config.get('agent_configs', []))}")
283
 
284
  # 创建模型
@@ -393,6 +406,12 @@ async def main():
393
  required=True,
394
  help='配置文件路径 (simulation_config.json)'
395
  )
 
 
 
 
 
 
396
 
397
  args = parser.parse_args()
398
 
@@ -405,7 +424,7 @@ async def main():
405
  setup_oasis_logging(os.path.join(simulation_dir, "log"))
406
 
407
  runner = TwitterSimulationRunner(args.config)
408
- await runner.run()
409
 
410
 
411
  if __name__ == "__main__":
 
259
 
260
  return active_agents
261
 
262
+ async def run(self, max_rounds: int = None):
263
+ """运行Twitter模拟
264
+
265
+ Args:
266
+ max_rounds: 最大模拟轮数(可选,用于截断过长的模拟)
267
+ """
268
  print("=" * 60)
269
  print("OASIS Twitter模拟")
270
  print(f"配置文件: {self.config_path}")
 
279
  # 计算总轮数
280
  total_rounds = (total_hours * 60) // minutes_per_round
281
 
282
+ # 如果指定了最大轮数,则截断
283
+ if max_rounds is not None and max_rounds > 0:
284
+ original_rounds = total_rounds
285
+ total_rounds = min(total_rounds, max_rounds)
286
+ if total_rounds < original_rounds:
287
+ print(f"\n轮数已截断: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})")
288
+
289
  print(f"\n模拟参数:")
290
  print(f" - 总模拟时长: {total_hours}小时")
291
  print(f" - 每轮时间: {minutes_per_round}分钟")
292
  print(f" - 总轮数: {total_rounds}")
293
+ if max_rounds:
294
+ print(f" - 最大轮数限制: {max_rounds}")
295
  print(f" - Agent数量: {len(self.config.get('agent_configs', []))}")
296
 
297
  # 创建模型
 
406
  required=True,
407
  help='配置文件路径 (simulation_config.json)'
408
  )
409
+ parser.add_argument(
410
+ '--max-rounds',
411
+ type=int,
412
+ default=None,
413
+ help='最大模拟轮数(可选,用于截断过长的模拟)'
414
+ )
415
 
416
  args = parser.parse_args()
417
 
 
424
  setup_oasis_logging(os.path.join(simulation_dir, "log"))
425
 
426
  runner = TwitterSimulationRunner(args.config)
427
+ await runner.run(max_rounds=args.max_rounds)
428
 
429
 
430
  if __name__ == "__main__":