MiroFish/backend/app/api/simulation.py
666ghj 5f159f6d88 Enhance backend functionality with OASIS simulation features
- Updated README.md to include new simulation scripts and configuration details for OASIS, including API retry mechanisms and environment variable settings.
- Added simulation management and configuration generation services to streamline the simulation process across Twitter and Reddit platforms.
- Introduced new API routes for simulation-related operations, including entity retrieval and simulation status management.
- Implemented a robust retry mechanism for external API calls to improve system stability.
- Enhanced task management model to include detailed progress tracking.
- Added logging capabilities for action tracking during simulations.
- Included new scripts for running parallel simulations and testing profile formats.
2025-12-01 15:03:44 +08:00

1330 lines
41 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.

"""
模拟相关API路由
Step2: Zep实体读取与过滤、OASIS模拟准备与运行全程自动化
"""
import os
import traceback
from flask import request, jsonify, send_file
from . import simulation_bp
from ..config import Config
from ..services.zep_entity_reader import ZepEntityReader
from ..services.oasis_profile_generator import OasisProfileGenerator
from ..services.simulation_manager import SimulationManager, SimulationStatus
from ..services.simulation_runner import SimulationRunner, RunnerStatus
from ..utils.logger import get_logger
from ..models.project import ProjectManager
logger = get_logger('mirofish.api.simulation')
# ============== 实体读取接口 ==============
@simulation_bp.route('/entities/<graph_id>', methods=['GET'])
def get_graph_entities(graph_id: str):
"""
获取图谱中的所有实体(已过滤)
只返回符合预定义实体类型的节点Labels不只是Entity的节点
Query参数
entity_types: 逗号分隔的实体类型列表(可选,用于进一步过滤)
enrich: 是否获取相关边信息默认true
"""
try:
if not Config.ZEP_API_KEY:
return jsonify({
"success": False,
"error": "ZEP_API_KEY未配置"
}), 500
entity_types_str = request.args.get('entity_types', '')
entity_types = [t.strip() for t in entity_types_str.split(',') if t.strip()] if entity_types_str else None
enrich = request.args.get('enrich', 'true').lower() == 'true'
logger.info(f"获取图谱实体: graph_id={graph_id}, entity_types={entity_types}, enrich={enrich}")
reader = ZepEntityReader()
result = reader.filter_defined_entities(
graph_id=graph_id,
defined_entity_types=entity_types,
enrich_with_edges=enrich
)
return jsonify({
"success": True,
"data": result.to_dict()
})
except Exception as e:
logger.error(f"获取图谱实体失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/entities/<graph_id>/<entity_uuid>', methods=['GET'])
def get_entity_detail(graph_id: str, entity_uuid: str):
"""获取单个实体的详细信息"""
try:
if not Config.ZEP_API_KEY:
return jsonify({
"success": False,
"error": "ZEP_API_KEY未配置"
}), 500
reader = ZepEntityReader()
entity = reader.get_entity_with_context(graph_id, entity_uuid)
if not entity:
return jsonify({
"success": False,
"error": f"实体不存在: {entity_uuid}"
}), 404
return jsonify({
"success": True,
"data": entity.to_dict()
})
except Exception as e:
logger.error(f"获取实体详情失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/entities/<graph_id>/by-type/<entity_type>', methods=['GET'])
def get_entities_by_type(graph_id: str, entity_type: str):
"""获取指定类型的所有实体"""
try:
if not Config.ZEP_API_KEY:
return jsonify({
"success": False,
"error": "ZEP_API_KEY未配置"
}), 500
enrich = request.args.get('enrich', 'true').lower() == 'true'
reader = ZepEntityReader()
entities = reader.get_entities_by_type(
graph_id=graph_id,
entity_type=entity_type,
enrich_with_edges=enrich
)
return jsonify({
"success": True,
"data": {
"entity_type": entity_type,
"count": len(entities),
"entities": [e.to_dict() for e in entities]
}
})
except Exception as e:
logger.error(f"获取实体失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== 模拟管理接口 ==============
@simulation_bp.route('/create', methods=['POST'])
def create_simulation():
"""
创建新的模拟
注意max_rounds等参数由LLM智能生成无需手动设置
请求JSON
{
"project_id": "proj_xxxx", // 必填
"graph_id": "mirofish_xxxx", // 可选如不提供则从project获取
"enable_twitter": true, // 可选默认true
"enable_reddit": true // 可选默认true
}
返回:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"project_id": "proj_xxxx",
"graph_id": "mirofish_xxxx",
"status": "created",
"enable_twitter": true,
"enable_reddit": true,
"created_at": "2025-12-01T10:00:00"
}
}
"""
try:
data = request.get_json() or {}
project_id = data.get('project_id')
if not project_id:
return jsonify({
"success": False,
"error": "请提供 project_id"
}), 400
project = ProjectManager.get_project(project_id)
if not project:
return jsonify({
"success": False,
"error": f"项目不存在: {project_id}"
}), 404
graph_id = data.get('graph_id') or project.graph_id
if not graph_id:
return jsonify({
"success": False,
"error": "项目尚未构建图谱,请先调用 /api/graph/build"
}), 400
manager = SimulationManager()
state = manager.create_simulation(
project_id=project_id,
graph_id=graph_id,
enable_twitter=data.get('enable_twitter', True),
enable_reddit=data.get('enable_reddit', True),
)
return jsonify({
"success": True,
"data": state.to_dict()
})
except Exception as e:
logger.error(f"创建模拟失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/prepare', methods=['POST'])
def prepare_simulation():
"""
准备模拟环境异步任务LLM智能生成所有参数
这是一个耗时操作接口会立即返回task_id
使用 GET /api/simulation/prepare/status 查询进度
步骤:
1. 从Zep图谱读取并过滤实体
2. 为每个实体生成OASIS Agent Profile带重试机制
3. LLM智能生成模拟配置带重试机制
4. 保存配置文件和预设脚本
请求JSON
{
"simulation_id": "sim_xxxx", // 必填模拟ID
"entity_types": ["Student", "PublicFigure"], // 可选,指定实体类型
"use_llm_for_profiles": true // 可选是否用LLM生成人设
}
返回:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"task_id": "task_xxxx",
"status": "preparing",
"message": "准备任务已启动"
}
}
"""
import threading
from ..models.task import TaskManager, TaskStatus
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
if not simulation_id:
return jsonify({
"success": False,
"error": "请提供 simulation_id"
}), 400
manager = SimulationManager()
state = manager.get_simulation(simulation_id)
if not state:
return jsonify({
"success": False,
"error": f"模拟不存在: {simulation_id}"
}), 404
# 从项目获取必要信息
project = ProjectManager.get_project(state.project_id)
if not project:
return jsonify({
"success": False,
"error": f"项目不存在: {state.project_id}"
}), 404
# 获取模拟需求
simulation_requirement = project.simulation_requirement or ""
if not simulation_requirement:
return jsonify({
"success": False,
"error": "项目缺少模拟需求描述 (simulation_requirement)"
}), 400
# 获取文档文本
document_text = ProjectManager.get_extracted_text(state.project_id) or ""
entity_types_list = data.get('entity_types')
use_llm_for_profiles = data.get('use_llm_for_profiles', True)
# 创建异步任务
task_manager = TaskManager()
task_id = task_manager.create_task(
task_type="simulation_prepare",
metadata={
"simulation_id": simulation_id,
"project_id": state.project_id
}
)
# 更新模拟状态
state.status = SimulationStatus.PREPARING
manager._save_simulation_state(state)
# 定义后台任务
def run_prepare():
try:
task_manager.update_task(
task_id,
status=TaskStatus.PROCESSING,
progress=0,
message="开始准备模拟环境..."
)
# 准备模拟(带进度回调)
# 存储阶段进度详情
stage_details = {}
def progress_callback(stage, progress, message, **kwargs):
# 计算总进度
stage_weights = {
"reading": (0, 20), # 0-20%
"generating_profiles": (20, 70), # 20-70%
"generating_config": (70, 90), # 70-90%
"copying_scripts": (90, 100) # 90-100%
}
start, end = stage_weights.get(stage, (0, 100))
current_progress = int(start + (end - start) * progress / 100)
# 构建详细进度信息
stage_names = {
"reading": "读取图谱实体",
"generating_profiles": "生成Agent人设",
"generating_config": "生成模拟配置",
"copying_scripts": "准备模拟脚本"
}
stage_index = list(stage_weights.keys()).index(stage) + 1 if stage in stage_weights else 1
total_stages = len(stage_weights)
# 更新阶段详情
stage_details[stage] = {
"stage_name": stage_names.get(stage, stage),
"stage_progress": progress,
"current": kwargs.get("current", 0),
"total": kwargs.get("total", 0),
"item_name": kwargs.get("item_name", "")
}
# 构建详细进度信息
detail = stage_details[stage]
progress_detail_data = {
"current_stage": stage,
"current_stage_name": stage_names.get(stage, stage),
"stage_index": stage_index,
"total_stages": total_stages,
"stage_progress": progress,
"current_item": detail["current"],
"total_items": detail["total"],
"item_description": message
}
# 构建简洁消息
if detail["total"] > 0:
detailed_message = (
f"[{stage_index}/{total_stages}] {stage_names.get(stage, stage)}: "
f"{detail['current']}/{detail['total']} - {message}"
)
else:
detailed_message = f"[{stage_index}/{total_stages}] {stage_names.get(stage, stage)}: {message}"
task_manager.update_task(
task_id,
progress=current_progress,
message=detailed_message,
progress_detail=progress_detail_data
)
result_state = manager.prepare_simulation(
simulation_id=simulation_id,
simulation_requirement=simulation_requirement,
document_text=document_text,
defined_entity_types=entity_types_list,
use_llm_for_profiles=use_llm_for_profiles,
progress_callback=progress_callback
)
# 任务完成
task_manager.complete_task(
task_id,
result=result_state.to_simple_dict()
)
except Exception as e:
logger.error(f"准备模拟失败: {str(e)}")
task_manager.fail_task(task_id, str(e))
# 更新模拟状态为失败
state = manager.get_simulation(simulation_id)
if state:
state.status = SimulationStatus.FAILED
state.error = str(e)
manager._save_simulation_state(state)
# 启动后台线程
thread = threading.Thread(target=run_prepare, daemon=True)
thread.start()
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"task_id": task_id,
"status": "preparing",
"message": "准备任务已启动,请通过 /api/simulation/prepare/status 查询进度"
}
})
except ValueError as e:
return jsonify({
"success": False,
"error": str(e)
}), 404
except Exception as e:
logger.error(f"启动准备任务失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/prepare/status', methods=['POST'])
def get_prepare_status():
"""
查询准备任务进度
请求JSON
{
"task_id": "task_xxxx" // 必填prepare返回的task_id
}
返回:
{
"success": true,
"data": {
"task_id": "task_xxxx",
"status": "processing", // pending/processing/completed/failed
"progress": 45, // 0-100 总进度
"message": "[2/4] 生成Agent人设: 35/93 - 生成 教授张三 的人设...",
"progress_detail": { // 详细进度信息
"current_stage": "generating_profiles",
"current_stage_name": "生成Agent人设",
"stage_index": 2, // 当前阶段序号
"total_stages": 4, // 总阶段数
"stage_progress": 38, // 阶段内进度 0-100
"current_item": 35, // 当前处理项目序号
"total_items": 93, // 当前阶段总项目数
"item_description": "生成 教授张三 的人设..."
},
"result": null, // 完成后返回结果
"error": null // 失败时返回错误信息
}
}
"""
from ..models.task import TaskManager
try:
data = request.get_json() or {}
task_id = data.get('task_id')
if not task_id:
return jsonify({
"success": False,
"error": "请提供 task_id"
}), 400
task_manager = TaskManager()
task = task_manager.get_task(task_id)
if not task:
return jsonify({
"success": False,
"error": f"任务不存在: {task_id}"
}), 404
return jsonify({
"success": True,
"data": task.to_dict()
})
except Exception as e:
logger.error(f"查询任务状态失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e)
}), 500
@simulation_bp.route('/<simulation_id>', methods=['GET'])
def get_simulation(simulation_id: str):
"""获取模拟状态"""
try:
manager = SimulationManager()
state = manager.get_simulation(simulation_id)
if not state:
return jsonify({
"success": False,
"error": f"模拟不存在: {simulation_id}"
}), 404
result = state.to_dict()
# 如果模拟已准备好,附加运行说明
if state.status == SimulationStatus.READY:
result["run_instructions"] = manager.get_run_instructions(simulation_id)
return jsonify({
"success": True,
"data": result
})
except Exception as e:
logger.error(f"获取模拟状态失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/list', methods=['GET'])
def list_simulations():
"""
列出所有模拟
Query参数
project_id: 按项目ID过滤可选
"""
try:
project_id = request.args.get('project_id')
manager = SimulationManager()
simulations = manager.list_simulations(project_id=project_id)
return jsonify({
"success": True,
"data": [s.to_dict() for s in simulations],
"count": len(simulations)
})
except Exception as e:
logger.error(f"列出模拟失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/profiles', methods=['GET'])
def get_simulation_profiles(simulation_id: str):
"""
获取模拟的Agent Profile
Query参数
platform: 平台类型reddit/twitter默认reddit
"""
try:
platform = request.args.get('platform', 'reddit')
manager = SimulationManager()
profiles = manager.get_profiles(simulation_id, platform=platform)
return jsonify({
"success": True,
"data": {
"platform": platform,
"count": len(profiles),
"profiles": profiles
}
})
except ValueError as e:
return jsonify({
"success": False,
"error": str(e)
}), 404
except Exception as e:
logger.error(f"获取Profile失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/config', methods=['GET'])
def get_simulation_config(simulation_id: str):
"""
获取模拟配置LLM智能生成的完整配置
返回包含:
- time_config: 时间配置(模拟时长、轮次、高峰/低谷时段)
- agent_configs: 每个Agent的活动配置活跃度、发言频率、立场等
- event_config: 事件配置(初始帖子、热点话题)
- platform_configs: 平台配置
- generation_reasoning: LLM的配置推理说明
"""
try:
manager = SimulationManager()
config = manager.get_simulation_config(simulation_id)
if not config:
return jsonify({
"success": False,
"error": f"模拟配置不存在,请先调用 /prepare 接口"
}), 404
return jsonify({
"success": True,
"data": config
})
except Exception as e:
logger.error(f"获取配置失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/config/download', methods=['GET'])
def download_simulation_config(simulation_id: str):
"""下载模拟配置文件"""
try:
manager = SimulationManager()
sim_dir = manager._get_simulation_dir(simulation_id)
config_path = os.path.join(sim_dir, "simulation_config.json")
if not os.path.exists(config_path):
return jsonify({
"success": False,
"error": "配置文件不存在,请先调用 /prepare 接口"
}), 404
return send_file(
config_path,
as_attachment=True,
download_name="simulation_config.json"
)
except Exception as e:
logger.error(f"下载配置失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/script/<script_name>/download', methods=['GET'])
def download_simulation_script(simulation_id: str, script_name: str):
"""
下载模拟脚本文件
script_name可选值
- run_twitter_simulation.py
- run_reddit_simulation.py
- run_parallel_simulation.py
"""
try:
manager = SimulationManager()
sim_dir = manager._get_simulation_dir(simulation_id)
# 验证脚本名称
allowed_scripts = [
"run_twitter_simulation.py",
"run_reddit_simulation.py",
"run_parallel_simulation.py"
]
if script_name not in allowed_scripts:
return jsonify({
"success": False,
"error": f"未知脚本: {script_name},可选: {allowed_scripts}"
}), 400
script_path = os.path.join(sim_dir, script_name)
if not os.path.exists(script_path):
return jsonify({
"success": False,
"error": "脚本文件不存在,请先调用 /prepare 接口"
}), 404
return send_file(
script_path,
as_attachment=True,
download_name=script_name
)
except Exception as e:
logger.error(f"下载脚本失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== Profile生成接口独立使用 ==============
@simulation_bp.route('/generate-profiles', methods=['POST'])
def generate_profiles():
"""
直接从图谱生成OASIS Agent Profile不创建模拟
请求JSON
{
"graph_id": "mirofish_xxxx", // 必填
"entity_types": ["Student"], // 可选
"use_llm": true, // 可选
"platform": "reddit" // 可选
}
"""
try:
data = request.get_json() or {}
graph_id = data.get('graph_id')
if not graph_id:
return jsonify({
"success": False,
"error": "请提供 graph_id"
}), 400
entity_types = data.get('entity_types')
use_llm = data.get('use_llm', True)
platform = data.get('platform', 'reddit')
reader = ZepEntityReader()
filtered = reader.filter_defined_entities(
graph_id=graph_id,
defined_entity_types=entity_types,
enrich_with_edges=True
)
if filtered.filtered_count == 0:
return jsonify({
"success": False,
"error": "没有找到符合条件的实体"
}), 400
generator = OasisProfileGenerator()
profiles = generator.generate_profiles_from_entities(
entities=filtered.entities,
use_llm=use_llm
)
if platform == "reddit":
profiles_data = [p.to_reddit_format() for p in profiles]
elif platform == "twitter":
profiles_data = [p.to_twitter_format() for p in profiles]
else:
profiles_data = [p.to_dict() for p in profiles]
return jsonify({
"success": True,
"data": {
"platform": platform,
"entity_types": list(filtered.entity_types),
"count": len(profiles_data),
"profiles": profiles_data
}
})
except Exception as e:
logger.error(f"生成Profile失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== 模拟运行控制接口 ==============
@simulation_bp.route('/start', methods=['POST'])
def start_simulation():
"""
开始运行模拟
请求JSON
{
"simulation_id": "sim_xxxx", // 必填模拟ID
"platform": "parallel" // 可选: twitter / reddit / parallel (默认)
}
返回:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"runner_status": "running",
"process_pid": 12345,
"twitter_running": true,
"reddit_running": true,
"started_at": "2025-12-01T10:00:00"
}
}
"""
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
if not simulation_id:
return jsonify({
"success": False,
"error": "请提供 simulation_id"
}), 400
platform = data.get('platform', 'parallel')
if platform not in ['twitter', 'reddit', 'parallel']:
return jsonify({
"success": False,
"error": f"无效的平台类型: {platform},可选: twitter/reddit/parallel"
}), 400
# 检查模拟是否已准备好
manager = SimulationManager()
state = manager.get_simulation(simulation_id)
if not state:
return jsonify({
"success": False,
"error": f"模拟不存在: {simulation_id}"
}), 404
if state.status != SimulationStatus.READY:
return jsonify({
"success": False,
"error": f"模拟未准备好,当前状态: {state.status.value},请先调用 /prepare 接口"
}), 400
# 启动模拟
run_state = SimulationRunner.start_simulation(simulation_id, platform)
# 更新模拟状态
state.status = SimulationStatus.RUNNING
manager._save_simulation_state(state)
return jsonify({
"success": True,
"data": run_state.to_dict()
})
except ValueError as e:
return jsonify({
"success": False,
"error": str(e)
}), 400
except Exception as e:
logger.error(f"启动模拟失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/stop', methods=['POST'])
def stop_simulation():
"""
停止模拟
请求JSON
{
"simulation_id": "sim_xxxx" // 必填模拟ID
}
返回:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"runner_status": "stopped",
"completed_at": "2025-12-01T12:00:00"
}
}
"""
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
if not simulation_id:
return jsonify({
"success": False,
"error": "请提供 simulation_id"
}), 400
run_state = SimulationRunner.stop_simulation(simulation_id)
# 更新模拟状态
manager = SimulationManager()
state = manager.get_simulation(simulation_id)
if state:
state.status = SimulationStatus.PAUSED
manager._save_simulation_state(state)
return jsonify({
"success": True,
"data": run_state.to_dict()
})
except ValueError as e:
return jsonify({
"success": False,
"error": str(e)
}), 400
except Exception as e:
logger.error(f"停止模拟失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== 实时状态监控接口 ==============
@simulation_bp.route('/<simulation_id>/run-status', methods=['GET'])
def get_run_status(simulation_id: str):
"""
获取模拟运行实时状态(用于前端轮询)
返回:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"runner_status": "running",
"current_round": 5,
"total_rounds": 144,
"progress_percent": 3.5,
"simulated_hours": 2,
"total_simulation_hours": 72,
"twitter_running": true,
"reddit_running": true,
"twitter_actions_count": 150,
"reddit_actions_count": 200,
"total_actions_count": 350,
"started_at": "2025-12-01T10:00:00",
"updated_at": "2025-12-01T10:30:00"
}
}
"""
try:
run_state = SimulationRunner.get_run_state(simulation_id)
if not run_state:
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"runner_status": "idle",
"current_round": 0,
"total_rounds": 0,
"progress_percent": 0,
"twitter_actions_count": 0,
"reddit_actions_count": 0,
"total_actions_count": 0,
}
})
return jsonify({
"success": True,
"data": run_state.to_dict()
})
except Exception as e:
logger.error(f"获取运行状态失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/run-status/detail', methods=['GET'])
def get_run_status_detail(simulation_id: str):
"""
获取模拟运行详细状态(包含最近动作)
用于前端展示实时动态
返回:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"runner_status": "running",
"current_round": 5,
...
"recent_actions": [
{
"round_num": 5,
"timestamp": "2025-12-01T10:30:00",
"platform": "twitter",
"agent_id": 3,
"agent_name": "Agent Name",
"action_type": "CREATE_POST",
"action_args": {"content": "..."},
"result": null,
"success": true
},
...
]
}
}
"""
try:
run_state = SimulationRunner.get_run_state(simulation_id)
if not run_state:
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"runner_status": "idle",
"recent_actions": []
}
})
return jsonify({
"success": True,
"data": run_state.to_detail_dict()
})
except Exception as e:
logger.error(f"获取详细状态失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/actions', methods=['GET'])
def get_simulation_actions(simulation_id: str):
"""
获取模拟中的Agent动作历史
Query参数
limit: 返回数量默认100
offset: 偏移量默认0
platform: 过滤平台twitter/reddit
agent_id: 过滤Agent ID
round_num: 过滤轮次
返回:
{
"success": true,
"data": {
"count": 100,
"actions": [...]
}
}
"""
try:
limit = request.args.get('limit', 100, type=int)
offset = request.args.get('offset', 0, type=int)
platform = request.args.get('platform')
agent_id = request.args.get('agent_id', type=int)
round_num = request.args.get('round_num', type=int)
actions = SimulationRunner.get_actions(
simulation_id=simulation_id,
limit=limit,
offset=offset,
platform=platform,
agent_id=agent_id,
round_num=round_num
)
return jsonify({
"success": True,
"data": {
"count": len(actions),
"actions": [a.to_dict() for a in actions]
}
})
except Exception as e:
logger.error(f"获取动作历史失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/timeline', methods=['GET'])
def get_simulation_timeline(simulation_id: str):
"""
获取模拟时间线(按轮次汇总)
用于前端展示进度条和时间线视图
Query参数
start_round: 起始轮次默认0
end_round: 结束轮次(默认全部)
返回每轮的汇总信息
"""
try:
start_round = request.args.get('start_round', 0, type=int)
end_round = request.args.get('end_round', type=int)
timeline = SimulationRunner.get_timeline(
simulation_id=simulation_id,
start_round=start_round,
end_round=end_round
)
return jsonify({
"success": True,
"data": {
"rounds_count": len(timeline),
"timeline": timeline
}
})
except Exception as e:
logger.error(f"获取时间线失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/agent-stats', methods=['GET'])
def get_agent_stats(simulation_id: str):
"""
获取每个Agent的统计信息
用于前端展示Agent活跃度排行、动作分布等
"""
try:
stats = SimulationRunner.get_agent_stats(simulation_id)
return jsonify({
"success": True,
"data": {
"agents_count": len(stats),
"stats": stats
}
})
except Exception as e:
logger.error(f"获取Agent统计失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== 数据库查询接口 ==============
@simulation_bp.route('/<simulation_id>/posts', methods=['GET'])
def get_simulation_posts(simulation_id: str):
"""
获取模拟中的帖子
Query参数
platform: 平台类型twitter/reddit
limit: 返回数量默认50
offset: 偏移量
返回帖子列表从SQLite数据库读取
"""
try:
platform = request.args.get('platform', 'reddit')
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
sim_dir = os.path.join(
os.path.dirname(__file__),
f'../../uploads/simulations/{simulation_id}'
)
db_file = f"{platform}_simulation.db"
db_path = os.path.join(sim_dir, db_file)
if not os.path.exists(db_path):
return jsonify({
"success": True,
"data": {
"platform": platform,
"count": 0,
"posts": [],
"message": "数据库不存在,模拟可能尚未运行"
}
})
import sqlite3
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
try:
cursor.execute("""
SELECT * FROM post
ORDER BY created_at DESC
LIMIT ? OFFSET ?
""", (limit, offset))
posts = [dict(row) for row in cursor.fetchall()]
cursor.execute("SELECT COUNT(*) FROM post")
total = cursor.fetchone()[0]
except sqlite3.OperationalError:
posts = []
total = 0
conn.close()
return jsonify({
"success": True,
"data": {
"platform": platform,
"total": total,
"count": len(posts),
"posts": posts
}
})
except Exception as e:
logger.error(f"获取帖子失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/comments', methods=['GET'])
def get_simulation_comments(simulation_id: str):
"""
获取模拟中的评论仅Reddit
Query参数
post_id: 过滤帖子ID可选
limit: 返回数量
offset: 偏移量
"""
try:
post_id = request.args.get('post_id')
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
sim_dir = os.path.join(
os.path.dirname(__file__),
f'../../uploads/simulations/{simulation_id}'
)
db_path = os.path.join(sim_dir, "reddit_simulation.db")
if not os.path.exists(db_path):
return jsonify({
"success": True,
"data": {
"count": 0,
"comments": []
}
})
import sqlite3
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
try:
if post_id:
cursor.execute("""
SELECT * FROM comment
WHERE post_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
""", (post_id, limit, offset))
else:
cursor.execute("""
SELECT * FROM comment
ORDER BY created_at DESC
LIMIT ? OFFSET ?
""", (limit, offset))
comments = [dict(row) for row in cursor.fetchall()]
except sqlite3.OperationalError:
comments = []
conn.close()
return jsonify({
"success": True,
"data": {
"count": len(comments),
"comments": comments
}
})
except Exception as e:
logger.error(f"获取评论失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500