This commit is contained in:
Cyrus Auyeung 2026-07-07 23:53:06 +08:00 committed by GitHub
commit 86afdbba77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 249 additions and 71 deletions

View File

@ -295,7 +295,7 @@ def build_graph():
}), 500 }), 500
# 解析请求 # 解析请求
data = request.get_json() or {} data = request.get_json(silent=True) or {}
project_id = data.get('project_id') project_id = data.get('project_id')
logger.debug(f"请求参数: project_id={project_id}") logger.debug(f"请求参数: project_id={project_id}")

View File

@ -48,7 +48,7 @@ def generate_report():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
if not simulation_id: if not simulation_id:
@ -223,7 +223,7 @@ def get_generate_status():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
task_id = data.get('task_id') task_id = data.get('task_id')
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
@ -497,7 +497,7 @@ def chat_with_report_agent():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
message = data.get('message') message = data.get('message')
@ -945,7 +945,7 @@ def search_graph_tool():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
graph_id = data.get('graph_id') graph_id = data.get('graph_id')
query = data.get('query') query = data.get('query')
@ -991,7 +991,7 @@ def get_graph_statistics_tool():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
graph_id = data.get('graph_id') graph_id = data.get('graph_id')

View File

@ -192,7 +192,7 @@ def create_simulation():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
project_id = data.get('project_id') project_id = data.get('project_id')
if not project_id: if not project_id:
@ -291,7 +291,7 @@ def _check_simulation_prepared(simulation_id: str) -> tuple:
state_file = os.path.join(simulation_dir, "state.json") state_file = os.path.join(simulation_dir, "state.json")
try: try:
import json import json
with open(state_file, 'r', encoding='utf-8') as f: with open(state_file, 'r', encoding='utf-8-sig') as f:
state_data = json.load(f) state_data = json.load(f)
status = state_data.get("status", "") status = state_data.get("status", "")
@ -307,8 +307,9 @@ def _check_simulation_prepared(simulation_id: str) -> tuple:
# - running: 正在运行,说明准备早就完成了 # - running: 正在运行,说明准备早就完成了
# - completed: 运行完成,说明准备早就完成了 # - completed: 运行完成,说明准备早就完成了
# - stopped: 已停止,说明准备早就完成了 # - stopped: 已停止,说明准备早就完成了
# - paused: 手动停止后会写入 paused配置仍然可复用
# - failed: 运行失败(但准备是完成的) # - failed: 运行失败(但准备是完成的)
prepared_statuses = ["ready", "preparing", "running", "completed", "stopped", "failed"] prepared_statuses = ["ready", "preparing", "running", "completed", "stopped", "paused", "failed"]
if status in prepared_statuses and config_generated: if status in prepared_statuses and config_generated:
# 获取文件统计信息 # 获取文件统计信息
profiles_file = os.path.join(simulation_dir, "reddit_profiles.json") profiles_file = os.path.join(simulation_dir, "reddit_profiles.json")
@ -403,7 +404,7 @@ def prepare_simulation():
from ..config import Config from ..config import Config
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
if not simulation_id: if not simulation_id:
@ -670,7 +671,7 @@ def get_prepare_status():
from ..models.task import TaskManager from ..models.task import TaskManager
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
task_id = data.get('task_id') task_id = data.get('task_id')
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
@ -1104,7 +1105,7 @@ def get_simulation_profiles_realtime(simulation_id: str):
state_file = os.path.join(sim_dir, "state.json") state_file = os.path.join(sim_dir, "state.json")
if os.path.exists(state_file): if os.path.exists(state_file):
try: try:
with open(state_file, 'r', encoding='utf-8') as f: with open(state_file, 'r', encoding='utf-8-sig') as f:
state_data = json.load(f) state_data = json.load(f)
status = state_data.get("status", "") status = state_data.get("status", "")
is_generating = status == "preparing" is_generating = status == "preparing"
@ -1200,7 +1201,7 @@ def get_simulation_config_realtime(simulation_id: str):
state_file = os.path.join(sim_dir, "state.json") state_file = os.path.join(sim_dir, "state.json")
if os.path.exists(state_file): if os.path.exists(state_file):
try: try:
with open(state_file, 'r', encoding='utf-8') as f: with open(state_file, 'r', encoding='utf-8-sig') as f:
state_data = json.load(f) state_data = json.load(f)
status = state_data.get("status", "") status = state_data.get("status", "")
is_generating = status == "preparing" is_generating = status == "preparing"
@ -1388,7 +1389,7 @@ def generate_profiles():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
graph_id = data.get('graph_id') graph_id = data.get('graph_id')
if not graph_id: if not graph_id:
@ -1490,7 +1491,7 @@ def start_simulation():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
if not simulation_id: if not simulation_id:
@ -1599,6 +1600,17 @@ def start_simulation():
}), 400 }), 400
logger.info(f"启用图谱记忆更新: simulation_id={simulation_id}, graph_id={graph_id}") logger.info(f"启用图谱记忆更新: simulation_id={simulation_id}, graph_id={graph_id}")
existing_run_state = SimulationRunner.get_run_state(simulation_id)
if existing_run_state and existing_run_state.runner_status.value not in ["starting", "running"]:
logger.info(
f"清理已结束的旧运行记录后重新启动: "
f"simulation_id={simulation_id}, runner_status={existing_run_state.runner_status.value}"
)
cleanup_result = SimulationRunner.cleanup_simulation_logs(simulation_id)
if not cleanup_result.get("success"):
logger.warning(f"清理旧运行记录时出现警告: {cleanup_result.get('errors')}")
force_restarted = True
# 启动模拟 # 启动模拟
run_state = SimulationRunner.start_simulation( run_state = SimulationRunner.start_simulation(
@ -1662,7 +1674,7 @@ def stop_simulation():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
if not simulation_id: if not simulation_id:
@ -2191,7 +2203,7 @@ def interview_agent():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
agent_id = data.get('agent_id') agent_id = data.get('agent_id')
@ -2313,7 +2325,7 @@ def interview_agents_batch():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
interviews = data.get('interviews') interviews = data.get('interviews')
@ -2440,7 +2452,7 @@ def interview_all_agents():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
prompt = data.get('prompt') prompt = data.get('prompt')
@ -2544,7 +2556,7 @@ def get_interview_history():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
platform = data.get('platform') # 不指定则返回两个平台的历史 platform = data.get('platform') # 不指定则返回两个平台的历史
@ -2606,7 +2618,7 @@ def get_env_status():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
@ -2673,7 +2685,7 @@ def close_simulation_env():
} }
""" """
try: try:
data = request.get_json() or {} data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
timeout = data.get('timeout', 30) timeout = data.get('timeout', 30)

View File

@ -195,7 +195,8 @@ class OasisProfileGenerator:
self.client = OpenAI( self.client = OpenAI(
api_key=self.api_key, api_key=self.api_key,
base_url=self.base_url base_url=self.base_url,
default_headers={"User-Agent": "python-requests/2.32.5"}
) )
# Zep客户端用于检索丰富上下文 # Zep客户端用于检索丰富上下文
@ -315,6 +316,7 @@ class OasisProfileGenerator:
return results return results
comprehensive_query = t('progress.zepSearchQuery', name=entity_name) comprehensive_query = t('progress.zepSearchQuery', name=entity_name)
zep_query = comprehensive_query[:400]
def search_edges(): def search_edges():
"""搜索边(事实/关系)- 带重试机制""" """搜索边(事实/关系)- 带重试机制"""
@ -325,7 +327,7 @@ class OasisProfileGenerator:
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
return self.zep_client.graph.search( return self.zep_client.graph.search(
query=comprehensive_query, query=zep_query,
graph_id=self.graph_id, graph_id=self.graph_id,
limit=30, limit=30,
scope="edges", scope="edges",
@ -350,7 +352,7 @@ class OasisProfileGenerator:
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
return self.zep_client.graph.search( return self.zep_client.graph.search(
query=comprehensive_query, query=zep_query,
graph_id=self.graph_id, graph_id=self.graph_id,
limit=20, limit=20,
scope="nodes", scope="nodes",

View File

@ -237,7 +237,8 @@ class SimulationConfigGenerator:
self.client = OpenAI( self.client = OpenAI(
api_key=self.api_key, api_key=self.api_key,
base_url=self.base_url base_url=self.base_url,
default_headers={"User-Agent": "python-requests/2.32.5"}
) )
def generate_config( def generate_config(

View File

@ -278,7 +278,7 @@ class SimulationIPCClient:
return False return False
try: try:
with open(status_file, 'r', encoding='utf-8') as f: with open(status_file, 'r', encoding='utf-8-sig') as f:
status = json.load(f) status = json.load(f)
return status.get("status") == "alive" return status.get("status") == "alive"
except (json.JSONDecodeError, OSError): except (json.JSONDecodeError, OSError):

View File

@ -165,7 +165,7 @@ class SimulationManager:
if not os.path.exists(state_file): if not os.path.exists(state_file):
return None return None
with open(state_file, 'r', encoding='utf-8') as f: with open(state_file, 'r', encoding='utf-8-sig') as f:
data = json.load(f) data = json.load(f)
state = SimulationState( state = SimulationState(

View File

@ -12,6 +12,7 @@ import threading
import subprocess import subprocess
import signal import signal
import atexit import atexit
import psutil
from typing import Dict, Any, List, Optional, Union from typing import Dict, Any, List, Optional, Union
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
@ -247,7 +248,7 @@ class SimulationRunner:
return None return None
try: try:
with open(state_file, 'r', encoding='utf-8') as f: with open(state_file, 'r', encoding='utf-8-sig') as f:
data = json.load(f) data = json.load(f)
state = SimulationRunState( state = SimulationRunState(
@ -1232,12 +1233,18 @@ class SimulationRunner:
# 更新 run_state.json # 更新 run_state.json
state = cls.get_run_state(simulation_id) state = cls.get_run_state(simulation_id)
keep_completed = bool(
state and (
state.runner_status == RunnerStatus.COMPLETED or
(state.progress_percent >= 100 and state.twitter_completed and state.reddit_completed)
)
)
if state: if state:
state.runner_status = RunnerStatus.STOPPED state.runner_status = RunnerStatus.COMPLETED if keep_completed else RunnerStatus.STOPPED
state.twitter_running = False state.twitter_running = False
state.reddit_running = False state.reddit_running = False
state.completed_at = datetime.now().isoformat() state.completed_at = state.completed_at or datetime.now().isoformat()
state.error = "服务器关闭,模拟被终止" state.error = None if keep_completed else "服务器关闭,模拟被终止"
cls._save_run_state(state) cls._save_run_state(state)
# 同时更新 state.json将状态设为 stopped # 同时更新 state.json将状态设为 stopped
@ -1246,13 +1253,13 @@ class SimulationRunner:
state_file = os.path.join(sim_dir, "state.json") state_file = os.path.join(sim_dir, "state.json")
logger.info(f"尝试更新 state.json: {state_file}") logger.info(f"尝试更新 state.json: {state_file}")
if os.path.exists(state_file): if os.path.exists(state_file):
with open(state_file, 'r', encoding='utf-8') as f: with open(state_file, 'r', encoding='utf-8-sig') as f:
state_data = json.load(f) state_data = json.load(f)
state_data['status'] = 'stopped' state_data['status'] = 'completed' if keep_completed else 'stopped'
state_data['updated_at'] = datetime.now().isoformat() state_data['updated_at'] = datetime.now().isoformat()
with open(state_file, 'w', encoding='utf-8') as f: with open(state_file, 'w', encoding='utf-8') as f:
json.dump(state_data, f, indent=2, ensure_ascii=False) json.dump(state_data, f, indent=2, ensure_ascii=False)
logger.info(f"已更新 state.json 状态为 stopped: {simulation_id}") logger.info(f"已更新 state.json 状态为 {state_data['status']}: {simulation_id}")
else: else:
logger.warning(f"state.json 不存在: {state_file}") logger.warning(f"state.json 不存在: {state_file}")
except Exception as state_err: except Exception as state_err:
@ -1386,7 +1393,23 @@ class SimulationRunner:
return False return False
ipc_client = SimulationIPCClient(sim_dir) ipc_client = SimulationIPCClient(sim_dir)
return ipc_client.check_env_alive() if not ipc_client.check_env_alive():
return False
process = cls._processes.get(simulation_id)
if process is not None:
return process.poll() is None
state = cls.get_run_state(simulation_id)
pid = state.process_pid if state else None
if not pid:
return False
try:
proc = psutil.Process(pid)
return proc.is_running() and proc.status() != psutil.STATUS_ZOMBIE
except psutil.Error:
return False
@classmethod @classmethod
def get_env_status_detail(cls, simulation_id: str) -> Dict[str, Any]: def get_env_status_detail(cls, simulation_id: str) -> Dict[str, Any]:
@ -1413,12 +1436,13 @@ class SimulationRunner:
return default_status return default_status
try: try:
with open(status_file, 'r', encoding='utf-8') as f: with open(status_file, 'r', encoding='utf-8-sig') as f:
status = json.load(f) status = json.load(f)
env_alive = cls.check_env_alive(simulation_id)
return { return {
"status": status.get("status", "stopped"), "status": status.get("status", "stopped") if env_alive else "stopped",
"twitter_available": status.get("twitter_available", False), "twitter_available": status.get("twitter_available", False) if env_alive else False,
"reddit_available": status.get("reddit_available", False), "reddit_available": status.get("reddit_available", False) if env_alive else False,
"timestamp": status.get("timestamp") "timestamp": status.get("timestamp")
} }
except (json.JSONDecodeError, OSError): except (json.JSONDecodeError, OSError):
@ -1459,7 +1483,7 @@ class SimulationRunner:
ipc_client = SimulationIPCClient(sim_dir) ipc_client = SimulationIPCClient(sim_dir)
if not ipc_client.check_env_alive(): if not cls.check_env_alive(simulation_id):
raise ValueError(f"模拟环境未运行或已关闭无法执行Interview: {simulation_id}") raise ValueError(f"模拟环境未运行或已关闭无法执行Interview: {simulation_id}")
logger.info(f"发送Interview命令: simulation_id={simulation_id}, agent_id={agent_id}, platform={platform}") logger.info(f"发送Interview命令: simulation_id={simulation_id}, agent_id={agent_id}, platform={platform}")
@ -1521,7 +1545,7 @@ class SimulationRunner:
ipc_client = SimulationIPCClient(sim_dir) ipc_client = SimulationIPCClient(sim_dir)
if not ipc_client.check_env_alive(): if not cls.check_env_alive(simulation_id):
raise ValueError(f"模拟环境未运行或已关闭无法执行Interview: {simulation_id}") raise ValueError(f"模拟环境未运行或已关闭无法执行Interview: {simulation_id}")
logger.info(f"发送批量Interview命令: simulation_id={simulation_id}, count={len(interviews)}, platform={platform}") logger.info(f"发送批量Interview命令: simulation_id={simulation_id}, count={len(interviews)}, platform={platform}")
@ -1631,7 +1655,7 @@ class SimulationRunner:
ipc_client = SimulationIPCClient(sim_dir) ipc_client = SimulationIPCClient(sim_dir)
if not ipc_client.check_env_alive(): if not cls.check_env_alive(simulation_id):
return { return {
"success": True, "success": True,
"message": "环境已经关闭" "message": "环境已经关闭"

View File

@ -484,13 +484,14 @@ class ZepToolsService:
SearchResult: 搜索结果 SearchResult: 搜索结果
""" """
logger.info(t("console.graphSearch", graphId=graph_id, query=query[:50])) logger.info(t("console.graphSearch", graphId=graph_id, query=query[:50]))
zep_query = query[:400]
# 尝试使用Zep Cloud Search API # 尝试使用Zep Cloud Search API
try: try:
search_results = self._call_with_retry( search_results = self._call_with_retry(
func=lambda: self.client.graph.search( func=lambda: self.client.graph.search(
graph_id=graph_id, graph_id=graph_id,
query=query, query=zep_query,
limit=limit, limit=limit,
scope=scope, scope=scope,
reranker="cross_encoder" reranker="cross_encoder"

View File

@ -29,7 +29,8 @@ class LLMClient:
self.client = OpenAI( self.client = OpenAI(
api_key=self.api_key, api_key=self.api_key,
base_url=self.base_url base_url=self.base_url,
default_headers={"User-Agent": "python-requests/2.32.5"}
) )
def chat( def chat(

View File

@ -32,6 +32,7 @@ dependencies = [
# 工具库 # 工具库
"python-dotenv>=1.0.0", "python-dotenv>=1.0.0",
"pydantic>=2.0.0", "pydantic>=2.0.0",
"psutil>=5.9.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]

View File

@ -33,3 +33,4 @@ python-dotenv>=1.0.0
# 数据验证 # 数据验证
pydantic>=2.0.0 pydantic>=2.0.0
psutil>=5.9.0

View File

@ -608,7 +608,7 @@ def load_config(config_path: str) -> Dict[str, Any]:
# 需要过滤掉的非核心动作类型(这些动作对分析价值较低) # 需要过滤掉的非核心动作类型(这些动作对分析价值较低)
FILTERED_ACTIONS = {'refresh', 'sign_up'} FILTERED_ACTIONS = {'refresh', 'sign_up', 'do_nothing'}
# 动作类型映射表(数据库中的名称 -> 标准名称) # 动作类型映射表(数据库中的名称 -> 标准名称)
ACTION_TYPE_MAP = { ACTION_TYPE_MAP = {
@ -678,8 +678,9 @@ def fetch_new_actions_from_db(
if not os.path.exists(db_path): if not os.path.exists(db_path):
return actions, new_last_rowid return actions, new_last_rowid
conn = None
try: try:
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path, timeout=5)
cursor = conn.cursor() cursor = conn.cursor()
# 使用 rowid 来追踪已处理的记录rowid 是 SQLite 的内置自增字段) # 使用 rowid 来追踪已处理的记录rowid 是 SQLite 的内置自增字段)
@ -739,13 +740,35 @@ def fetch_new_actions_from_db(
'action_args': simplified_args, 'action_args': simplified_args,
}) })
conn.close()
except Exception as e: except Exception as e:
print(f"读取数据库动作失败: {e}") print(f"读取数据库动作失败: {e}")
finally:
if conn is not None:
conn.close()
return actions, new_last_rowid return actions, new_last_rowid
def get_latest_trace_rowid(db_path: str) -> int:
"""Return the latest processed trace row id for a platform DB."""
if not os.path.exists(db_path):
return 0
conn = None
try:
conn = sqlite3.connect(db_path, timeout=5)
cursor = conn.cursor()
cursor.execute("SELECT COALESCE(MAX(rowid), 0) FROM trace")
rowid = int(cursor.fetchone()[0] or 0)
return rowid
except Exception as e:
print(f"读取最新动作游标失败: {e}")
return 0
finally:
if conn is not None:
conn.close()
def _enrich_action_context( def _enrich_action_context(
cursor, cursor,
action_type: str, action_type: str,
@ -1034,6 +1057,9 @@ def create_model(config: Dict[str, Any], use_boost: bool = False):
return ModelFactory.create( return ModelFactory.create(
model_platform=ModelPlatformType.OPENAI, model_platform=ModelPlatformType.OPENAI,
model_type=llm_model, model_type=llm_model,
api_key=llm_api_key or None,
url=llm_base_url or None,
default_headers={"User-Agent": "python-requests/2.32.5"},
) )
@ -1184,10 +1210,16 @@ async def run_twitter_simulation(
content = post.get("content", "") content = post.get("content", "")
try: try:
agent = result.env.agent_graph.get_agent(agent_id) agent = result.env.agent_graph.get_agent(agent_id)
initial_actions[agent] = ManualAction( manual_action = ManualAction(
action_type=ActionType.CREATE_POST, action_type=ActionType.CREATE_POST,
action_args={"content": content} action_args={"content": content}
) )
if agent in initial_actions:
if not isinstance(initial_actions[agent], list):
initial_actions[agent] = [initial_actions[agent]]
initial_actions[agent].append(manual_action)
else:
initial_actions[agent] = manual_action
if action_logger: if action_logger:
action_logger.log_action( action_logger.log_action(
@ -1204,7 +1236,9 @@ async def run_twitter_simulation(
if initial_actions: if initial_actions:
await result.env.step(initial_actions) await result.env.step(initial_actions)
log_info(f"已发布 {len(initial_actions)} 条初始帖子") last_rowid = get_latest_trace_rowid(db_path)
posted_count = sum(len(action) if isinstance(action, list) else 1 for action in initial_actions.values())
log_info(f"已发布 {posted_count} 条初始帖子")
# 记录 round 0 结束 # 记录 round 0 结束
if action_logger: if action_logger:
@ -1403,7 +1437,9 @@ async def run_reddit_simulation(
if initial_actions: if initial_actions:
await result.env.step(initial_actions) await result.env.step(initial_actions)
log_info(f"已发布 {len(initial_actions)} 条初始帖子") last_rowid = get_latest_trace_rowid(db_path)
posted_count = sum(len(action) if isinstance(action, list) else 1 for action in initial_actions.values())
log_info(f"已发布 {posted_count} 条初始帖子")
# 记录 round 0 结束 # 记录 round 0 结束
if action_logger: if action_logger:

View File

@ -464,6 +464,9 @@ class RedditSimulationRunner:
return ModelFactory.create( return ModelFactory.create(
model_platform=ModelPlatformType.OPENAI, model_platform=ModelPlatformType.OPENAI,
model_type=llm_model, model_type=llm_model,
api_key=llm_api_key or None,
url=llm_base_url or None,
default_headers={"User-Agent": "python-requests/2.32.5"},
) )
def _get_active_agents_for_round( def _get_active_agents_for_round(
@ -617,7 +620,8 @@ class RedditSimulationRunner:
if initial_actions: if initial_actions:
await self.env.step(initial_actions) await self.env.step(initial_actions)
print(f" 已发布 {len(initial_actions)} 条初始帖子") posted_count = sum(len(action) if isinstance(action, list) else 1 for action in initial_actions.values())
print(f" 已发布 {posted_count} 条初始帖子")
# 主模拟循环 # 主模拟循环
print("\n开始模拟循环...") print("\n开始模拟循环...")

View File

@ -457,6 +457,9 @@ class TwitterSimulationRunner:
return ModelFactory.create( return ModelFactory.create(
model_platform=ModelPlatformType.OPENAI, model_platform=ModelPlatformType.OPENAI,
model_type=llm_model, model_type=llm_model,
api_key=llm_api_key or None,
url=llm_base_url or None,
default_headers={"User-Agent": "python-requests/2.32.5"},
) )
def _get_active_agents_for_round( def _get_active_agents_for_round(
@ -615,16 +618,23 @@ class TwitterSimulationRunner:
content = post.get("content", "") content = post.get("content", "")
try: try:
agent = self.env.agent_graph.get_agent(agent_id) agent = self.env.agent_graph.get_agent(agent_id)
initial_actions[agent] = ManualAction( manual_action = ManualAction(
action_type=ActionType.CREATE_POST, action_type=ActionType.CREATE_POST,
action_args={"content": content} action_args={"content": content}
) )
if agent in initial_actions:
if not isinstance(initial_actions[agent], list):
initial_actions[agent] = [initial_actions[agent]]
initial_actions[agent].append(manual_action)
else:
initial_actions[agent] = manual_action
except Exception as e: except Exception as e:
print(f" 警告: 无法为Agent {agent_id}创建初始帖子: {e}") print(f" 警告: 无法为Agent {agent_id}创建初始帖子: {e}")
if initial_actions: if initial_actions:
await self.env.step(initial_actions) await self.env.step(initial_actions)
print(f" 已发布 {len(initial_actions)} 条初始帖子") posted_count = sum(len(action) if isinstance(action, list) else 1 for action in initial_actions.values())
print(f" 已发布 {posted_count} 条初始帖子")
# 主模拟循环 # 主模拟循环
print("\n开始模拟循环...") print("\n开始模拟循环...")

View File

@ -995,6 +995,7 @@ dependencies = [
{ name = "flask" }, { name = "flask" },
{ name = "flask-cors" }, { name = "flask-cors" },
{ name = "openai" }, { name = "openai" },
{ name = "psutil" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pymupdf" }, { name = "pymupdf" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
@ -1024,6 +1025,7 @@ requires-dist = [
{ name = "flask-cors", specifier = ">=6.0.0" }, { name = "flask-cors", specifier = ">=6.0.0" },
{ name = "openai", specifier = ">=1.0.0" }, { name = "openai", specifier = ">=1.0.0" },
{ name = "pipreqs", marker = "extra == 'dev'", specifier = ">=0.5.0" }, { name = "pipreqs", marker = "extra == 'dev'", specifier = ">=0.5.0" },
{ name = "psutil", specifier = ">=5.9.0" },
{ name = "pydantic", specifier = ">=2.0.0" }, { name = "pydantic", specifier = ">=2.0.0" },
{ name = "pymupdf", specifier = ">=1.24.0" }, { name = "pymupdf", specifier = ">=1.24.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },

View File

@ -106,9 +106,9 @@
<!-- Main Content: Dual Timeline --> <!-- Main Content: Dual Timeline -->
<div class="main-content-area" ref="scrollContainer"> <div class="main-content-area" ref="scrollContainer">
<!-- Timeline Header --> <!-- Timeline Header -->
<div class="timeline-header" v-if="allActions.length > 0"> <div class="timeline-header" v-if="chronologicalActions.length > 0">
<div class="timeline-stats"> <div class="timeline-stats">
<span class="total-count">TOTAL EVENTS: <span class="mono">{{ allActions.length }}</span></span> <span class="total-count">TOTAL EVENTS: <span class="mono">{{ chronologicalActions.length }}</span></span>
<span class="platform-breakdown"> <span class="platform-breakdown">
<span class="breakdown-item twitter"> <span class="breakdown-item twitter">
<svg class="mini-icon" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg> <svg class="mini-icon" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
@ -189,15 +189,32 @@
</div> </div>
</template> </template>
<!-- LIKE_POST: 点赞帖子 --> <!-- LIKE_POST / DISLIKE_POST: 点赞/帖子 -->
<template v-if="action.action_type === 'LIKE_POST'"> <template v-if="action.action_type === 'LIKE_POST' || action.action_type === 'DISLIKE_POST'">
<div class="like-info"> <div class="like-info">
<svg class="icon-small filled" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg> <svg class="icon-small filled" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
<span class="like-label">Liked @{{ action.action_args?.post_author_name || 'User' }}'s post</span> <span class="like-label">{{ action.action_type === 'DISLIKE_POST' ? 'Disliked' : 'Liked' }} @{{ action.action_args?.post_author_name || 'User' }}'s post</span>
</div> </div>
<div v-if="action.action_args?.post_content" class="liked-content"> <div v-if="action.action_args?.post_content" class="liked-content">
"{{ truncateContent(action.action_args.post_content, 120) }}" "{{ truncateContent(action.action_args.post_content, 120) }}"
</div> </div>
<div v-else-if="action.action_args?.post_id" class="comment-context">
<span>Post #{{ action.action_args.post_id }}</span>
</div>
</template>
<!-- LIKE_COMMENT / DISLIKE_COMMENT: 点赞/踩评论 -->
<template v-if="action.action_type === 'LIKE_COMMENT' || action.action_type === 'DISLIKE_COMMENT'">
<div class="like-info">
<svg class="icon-small filled" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
<span class="like-label">{{ action.action_type === 'DISLIKE_COMMENT' ? 'Disliked' : 'Liked' }} @{{ action.action_args?.comment_author_name || 'User' }}'s comment</span>
</div>
<div v-if="action.action_args?.comment_content" class="liked-content">
"{{ truncateContent(action.action_args.comment_content, 120) }}"
</div>
<div v-else-if="action.action_args?.comment_id" class="comment-context">
<span>Comment #{{ action.action_args.comment_id }}</span>
</div>
</template> </template>
<!-- CREATE_COMMENT: 发表评论 --> <!-- CREATE_COMMENT: 发表评论 -->
@ -224,7 +241,7 @@
<template v-if="action.action_type === 'FOLLOW'"> <template v-if="action.action_type === 'FOLLOW'">
<div class="follow-info"> <div class="follow-info">
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg> <svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg>
<span class="follow-label">Followed @{{ action.action_args?.target_user || action.action_args?.user_id || 'User' }}</span> <span class="follow-label">Followed @{{ action.action_args?.target_user_name || action.action_args?.target_user || action.action_args?.user_id || 'User' }}</span>
</div> </div>
</template> </template>
@ -249,7 +266,7 @@
</template> </template>
<!-- 通用回退未知类型或有 content 但未被上述处理 --> <!-- 通用回退未知类型或有 content 但未被上述处理 -->
<div v-if="!['CREATE_POST', 'QUOTE_POST', 'REPOST', 'LIKE_POST', 'CREATE_COMMENT', 'SEARCH_POSTS', 'FOLLOW', 'UPVOTE_POST', 'DOWNVOTE_POST', 'DO_NOTHING'].includes(action.action_type) && action.action_args?.content" class="content-text"> <div v-if="!['CREATE_POST', 'QUOTE_POST', 'REPOST', 'LIKE_POST', 'DISLIKE_POST', 'LIKE_COMMENT', 'DISLIKE_COMMENT', 'CREATE_COMMENT', 'SEARCH_POSTS', 'FOLLOW', 'UPVOTE_POST', 'DOWNVOTE_POST', 'DO_NOTHING'].includes(action.action_type) && action.action_args?.content" class="content-text">
{{ action.action_args.content }} {{ action.action_args.content }}
</div> </div>
</div> </div>
@ -262,7 +279,7 @@
</div> </div>
</TransitionGroup> </TransitionGroup>
<div v-if="allActions.length === 0" class="waiting-state"> <div v-if="chronologicalActions.length === 0" class="waiting-state">
<div class="pulse-ring"></div> <div class="pulse-ring"></div>
<span>Waiting for agent actions...</span> <span>Waiting for agent actions...</span>
</div> </div>
@ -326,19 +343,21 @@ const allActions = ref([]) // 所有动作(增量累积)
const actionIds = ref(new Set()) // ID const actionIds = ref(new Set()) // ID
const scrollContainer = ref(null) const scrollContainer = ref(null)
const isDisplayableAction = (action) => action?.action_type !== 'DO_NOTHING'
// Computed // Computed
// //
const chronologicalActions = computed(() => { const chronologicalActions = computed(() => {
return allActions.value return allActions.value.filter(isDisplayableAction)
}) })
// //
const twitterActionsCount = computed(() => { const twitterActionsCount = computed(() => {
return allActions.value.filter(a => a.platform === 'twitter').length return chronologicalActions.value.filter(a => a.platform === 'twitter').length
}) })
const redditActionsCount = computed(() => { const redditActionsCount = computed(() => {
return allActions.value.filter(a => a.platform === 'reddit').length return chronologicalActions.value.filter(a => a.platform === 'reddit').length
}) })
// //
@ -379,15 +398,75 @@ const resetAllState = () => {
stopPolling() // stopPolling() //
} }
const hasExistingRunData = (data) => {
if (!data) return false
const runnerStatus = data.runner_status
const actionsCount = Number(data.total_actions_count || 0)
+ Number(data.twitter_actions_count || 0)
+ Number(data.reddit_actions_count || 0)
return Boolean(data.started_at)
|| actionsCount > 0
|| ['starting', 'running', 'completed', 'stopped', 'paused', 'failed'].includes(runnerStatus)
}
const isActiveRun = (data) => {
return data?.runner_status === 'starting'
|| data?.runner_status === 'running'
|| data?.twitter_running
|| data?.reddit_running
}
const restoreExistingRun = async (data) => {
runStatus.value = data
prevTwitterRound.value = data.twitter_current_round || 0
prevRedditRound.value = data.reddit_current_round || 0
await fetchRunStatusDetail()
if (isActiveRun(data)) {
phase.value = 1
emit('update-status', 'processing')
startStatusPolling()
startDetailPolling()
return
}
phase.value = 2
emit('update-status', data.runner_status === 'failed' ? 'error' : 'completed')
if (data.error) {
addLog(t('log.startFailed', { error: data.error }))
}
}
const initializeSimulationView = async () => {
if (!props.simulationId) {
addLog(t('log.errorMissingSimId'))
return
}
resetAllState()
try {
const res = await getRunStatus(props.simulationId)
if (res.success && hasExistingRunData(res.data)) {
await restoreExistingRun(res.data)
return
}
} catch (err) {
console.warn('恢复模拟运行状态失败:', err)
}
await doStartSimulation({ reset: false })
}
// //
const doStartSimulation = async () => { const doStartSimulation = async ({ reset = true } = {}) => {
if (!props.simulationId) { if (!props.simulationId) {
addLog(t('log.errorMissingSimId')) addLog(t('log.errorMissingSimId'))
return return
} }
// //
resetAllState() if (reset) resetAllState()
isStarting.value = true isStarting.value = true
startError.value = null startError.value = null
@ -398,7 +477,7 @@ const doStartSimulation = async () => {
const params = { const params = {
simulation_id: props.simulationId, simulation_id: props.simulationId,
platform: 'parallel', platform: 'parallel',
force: true, // force: false,
enable_graph_memory_update: true // enable_graph_memory_update: true //
} }
@ -597,8 +676,10 @@ const getActionTypeLabel = (type) => {
'CREATE_POST': 'POST', 'CREATE_POST': 'POST',
'REPOST': 'REPOST', 'REPOST': 'REPOST',
'LIKE_POST': 'LIKE', 'LIKE_POST': 'LIKE',
'DISLIKE_POST': 'DISLIKE',
'CREATE_COMMENT': 'COMMENT', 'CREATE_COMMENT': 'COMMENT',
'LIKE_COMMENT': 'LIKE', 'LIKE_COMMENT': 'LIKE',
'DISLIKE_COMMENT': 'DISLIKE',
'DO_NOTHING': 'IDLE', 'DO_NOTHING': 'IDLE',
'FOLLOW': 'FOLLOW', 'FOLLOW': 'FOLLOW',
'SEARCH_POSTS': 'SEARCH', 'SEARCH_POSTS': 'SEARCH',
@ -614,8 +695,10 @@ const getActionTypeClass = (type) => {
'CREATE_POST': 'badge-post', 'CREATE_POST': 'badge-post',
'REPOST': 'badge-action', 'REPOST': 'badge-action',
'LIKE_POST': 'badge-action', 'LIKE_POST': 'badge-action',
'DISLIKE_POST': 'badge-action',
'CREATE_COMMENT': 'badge-comment', 'CREATE_COMMENT': 'badge-comment',
'LIKE_COMMENT': 'badge-action', 'LIKE_COMMENT': 'badge-action',
'DISLIKE_COMMENT': 'badge-action',
'QUOTE_POST': 'badge-post', 'QUOTE_POST': 'badge-post',
'FOLLOW': 'badge-meta', 'FOLLOW': 'badge-meta',
'SEARCH_POSTS': 'badge-meta', 'SEARCH_POSTS': 'badge-meta',
@ -690,7 +773,7 @@ watch(() => props.systemLogs?.length, () => {
onMounted(() => { onMounted(() => {
addLog(t('log.step3Init')) addLog(t('log.step3Init'))
if (props.simulationId) { if (props.simulationId) {
doStartSimulation() initializeSimulationView()
} }
}) })
@ -1124,7 +1207,7 @@ onUnmounted(() => {
} }
/* Info Blocks (Quote, Repost, etc) */ /* Info Blocks (Quote, Repost, etc) */
.quoted-block, .repost-content { .quoted-block, .repost-content, .liked-content, .voted-content {
background: #F9F9F9; background: #F9F9F9;
border: 1px solid #EEE; border: 1px solid #EEE;
padding: 10px 12px; padding: 10px 12px;
@ -1264,4 +1347,4 @@ onUnmounted(() => {
animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite;
margin-right: 6px; margin-right: 6px;
} }
</style> </style>