run.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. """
  2. 示例(流程对齐版)
  3. 参考 examples/research/run.py:
  4. 1. 使用框架 InteractiveController 统一交互流程
  5. 2. 使用 config.py 管理运行参数
  6. 3. 保留 create 场景特有的 prompt 注入与详细消息打印
  7. """
  8. import argparse
  9. import asyncio
  10. import copy
  11. import json
  12. import os
  13. import sys
  14. from pathlib import Path
  15. from typing import Any
  16. # Clash Verge TUN 模式兼容:禁止 httpx/urllib 自动检测系统 HTTP 代理
  17. os.environ.setdefault("no_proxy", "*")
  18. # 添加项目根目录到 Python 路径
  19. sys.path.insert(0, str(Path(__file__).parent.parent.parent))
  20. from dotenv import load_dotenv
  21. load_dotenv()
  22. from agent.cli import InteractiveController
  23. from agent.core.presets import AgentPreset, register_preset
  24. from agent.core.runner import AgentRunner
  25. from agent.llm import create_openrouter_llm_call
  26. from agent.llm.prompts import SimplePrompt
  27. from agent.trace import FileSystemTraceStore, Message, Trace
  28. from agent.utils import setup_logging
  29. from examples.content_needs_generation.html import trace_to_html
  30. # 导入项目配置
  31. from config import DEBUG, LOG_FILE, LOG_LEVEL, RUN_CONFIG, SKILLS_DIR, TRACE_STORE_PATH
  32. # 导入自定义工具模块,触发 @tool 装饰器注册
  33. import examples.content_needs_generation.tool # noqa: F401
  34. def _format_json(obj: Any, indent: int = 2) -> str:
  35. """格式化 JSON 对象为字符串"""
  36. try:
  37. return json.dumps(obj, indent=indent, ensure_ascii=False)
  38. except (TypeError, ValueError):
  39. return str(obj)
  40. def _print_message_details(message: Message):
  41. """完整打印消息的详细信息"""
  42. print("\n" + "=" * 80)
  43. print(f"[Message #{message.sequence}] {message.role.upper()}")
  44. print("=" * 80)
  45. if message.goal_id:
  46. print(f"Goal ID: {message.goal_id}")
  47. if message.parent_sequence is not None:
  48. print(f"Parent Sequence: {message.parent_sequence}")
  49. if message.tool_call_id:
  50. print(f"Tool Call ID: {message.tool_call_id}")
  51. if message.role == "user":
  52. print("\n[输入内容]")
  53. print("-" * 80)
  54. if isinstance(message.content, str):
  55. print(message.content)
  56. else:
  57. print(_format_json(message.content))
  58. elif message.role == "assistant":
  59. content = message.content
  60. if isinstance(content, dict):
  61. text = content.get("text", "")
  62. tool_calls = content.get("tool_calls")
  63. if text:
  64. print("\n[LLM 文本回复]")
  65. print("-" * 80)
  66. print(text)
  67. if tool_calls:
  68. print(f"\n[工具调用] (共 {len(tool_calls)} 个)")
  69. print("-" * 80)
  70. for idx, tc in enumerate(tool_calls, 1):
  71. func = tc.get("function", {})
  72. tool_name = func.get("name", "unknown")
  73. tool_id = tc.get("id", "unknown")
  74. arguments = func.get("arguments", {})
  75. print(f"\n工具 #{idx}: {tool_name}")
  76. print(f" Call ID: {tool_id}")
  77. print(" 参数:")
  78. if isinstance(arguments, str):
  79. try:
  80. parsed_args = json.loads(arguments)
  81. print(_format_json(parsed_args, indent=4))
  82. except json.JSONDecodeError:
  83. print(f" {arguments}")
  84. else:
  85. print(_format_json(arguments, indent=4))
  86. elif isinstance(content, str):
  87. print("\n[LLM 文本回复]")
  88. print("-" * 80)
  89. print(content)
  90. else:
  91. print("\n[内容]")
  92. print("-" * 80)
  93. print(_format_json(content))
  94. if message.finish_reason:
  95. print(f"\n完成原因: {message.finish_reason}")
  96. elif message.role == "tool":
  97. content = message.content
  98. print("\n[工具执行结果]")
  99. print("-" * 80)
  100. if isinstance(content, dict):
  101. tool_name = content.get("tool_name", "unknown")
  102. result = content.get("result", content)
  103. print(f"工具名称: {tool_name}")
  104. print("\n返回结果:")
  105. if isinstance(result, str):
  106. print(result)
  107. elif isinstance(result, list):
  108. for idx, item in enumerate(result, 1):
  109. if isinstance(item, dict) and item.get("type") == "image_url":
  110. print(f" [{idx}] 图片 (base64, 已省略显示)")
  111. else:
  112. print(f" [{idx}] {item}")
  113. else:
  114. print(_format_json(result))
  115. else:
  116. print(str(content) if content is not None else "(无内容)")
  117. elif message.role == "system":
  118. print("\n[系统提示]")
  119. print("-" * 80)
  120. if isinstance(message.content, str):
  121. print(message.content)
  122. else:
  123. print(_format_json(message.content))
  124. if message.prompt_tokens is not None or message.completion_tokens is not None:
  125. print("\n[Token 使用]")
  126. print("-" * 80)
  127. if message.prompt_tokens is not None:
  128. print(f" 输入 Tokens: {message.prompt_tokens:,}")
  129. if message.completion_tokens is not None:
  130. print(f" 输出 Tokens: {message.completion_tokens:,}")
  131. if message.reasoning_tokens is not None:
  132. print(f" 推理 Tokens: {message.reasoning_tokens:,}")
  133. if message.cache_creation_tokens is not None:
  134. print(f" 缓存创建 Tokens: {message.cache_creation_tokens:,}")
  135. if message.cache_read_tokens is not None:
  136. print(f" 缓存读取 Tokens: {message.cache_read_tokens:,}")
  137. if message.tokens:
  138. print(f" 总计 Tokens: {message.tokens:,}")
  139. if message.cost is not None:
  140. print(f"\n[成本] ${message.cost:.6f}")
  141. if message.duration_ms is not None:
  142. print(f"[执行时间] {message.duration_ms}ms")
  143. print("=" * 80 + "\n")
  144. def _apply_prompt_placeholders(base_dir: Path, prompt: SimplePrompt):
  145. """把 PRD 文件内容注入 prompt 占位符。"""
  146. system_md_path = base_dir / "PRD" / "system.md"
  147. if system_md_path.exists():
  148. system_content = system_md_path.read_text(encoding="utf-8")
  149. if "system" in prompt._messages and "{system}" in prompt._messages["system"]:
  150. prompt._messages["system"] = prompt._messages["system"].replace("{system}", system_content)
  151. create_process_md_path = base_dir / "PRD" / "business.md"
  152. if create_process_md_path.exists():
  153. create_process_content = create_process_md_path.read_text(encoding="utf-8")
  154. if "system" in prompt._messages and "{business}" in prompt._messages["system"]:
  155. prompt._messages["system"] = prompt._messages["system"].replace("{business}", create_process_content)
  156. output_md_path = base_dir / "PRD" / "output.md"
  157. if output_md_path.exists():
  158. output_content = output_md_path.read_text(encoding="utf-8")
  159. if "user" in prompt._messages and "{output}" in prompt._messages["user"]:
  160. prompt._messages["user"] = prompt._messages["user"].replace("{output}", output_content)
  161. async def main():
  162. parser = argparse.ArgumentParser(description="任务 (Agent 模式 + 交互增强)")
  163. parser.add_argument(
  164. "--trace",
  165. type=str,
  166. default=None,
  167. help="已有的 Trace ID,用于恢复继续执行(不指定则新建)",
  168. )
  169. args = parser.parse_args()
  170. base_dir = Path(__file__).parent
  171. prompt_path = base_dir / "content_needs_generation.prompt"
  172. output_dir = base_dir / "output"
  173. output_dir.mkdir(exist_ok=True)
  174. setup_logging(level=LOG_LEVEL, file=LOG_FILE)
  175. presets_path = base_dir / "presets.json"
  176. if presets_path.exists():
  177. with open(presets_path, "r", encoding="utf-8") as f:
  178. project_presets = json.load(f)
  179. for name, cfg in project_presets.items():
  180. register_preset(name, AgentPreset(**cfg))
  181. prompt = SimplePrompt(prompt_path)
  182. _apply_prompt_placeholders(base_dir, prompt)
  183. messages = prompt.build_messages()
  184. model_from_prompt = prompt.config.get("model")
  185. model_from_config = RUN_CONFIG.model
  186. default_model = f"anthropic/{model_from_config}" if "/" not in model_from_config else model_from_config
  187. model = model_from_prompt or default_model
  188. skills_dir = str((base_dir / SKILLS_DIR).resolve()) if not Path(SKILLS_DIR).is_absolute() else SKILLS_DIR
  189. # 验证 skills 目录是否存在
  190. skills_path = Path(skills_dir)
  191. if not skills_path.exists():
  192. print(f"⚠️ 警告: Skills 目录不存在: {skills_dir}")
  193. else:
  194. skill_files = list(skills_path.glob("*.md"))
  195. print(f"✓ 找到 {len(skill_files)} 个 skill 文件: {[f.name for f in skill_files]}")
  196. # 验证工具是否已注册
  197. from agent.tools import get_tool_registry
  198. tool_registry = get_tool_registry()
  199. registered_tools = list(tool_registry._tools.keys())
  200. custom_tools = [
  201. t for t in registered_tools
  202. if "hot_rank" in t.lower() or "content_deconstruction" in t.lower() or "query_content" in t.lower()
  203. ]
  204. if custom_tools:
  205. print(f"✓ 已注册自定义工具: {custom_tools}")
  206. else:
  207. print(f"⚠️ 警告: 未找到自定义工具")
  208. print(f" 已注册的工具: {registered_tools[:10]}...") # 只显示前10个
  209. store = FileSystemTraceStore(base_path=TRACE_STORE_PATH)
  210. runner = AgentRunner(
  211. trace_store=store,
  212. llm_call=create_openrouter_llm_call(model=model),
  213. skills_dir=skills_dir,
  214. debug=DEBUG,
  215. )
  216. interactive = InteractiveController(
  217. runner=runner,
  218. store=store,
  219. enable_stdin_check=True,
  220. )
  221. task_name = RUN_CONFIG.name or base_dir.name
  222. print("=" * 60)
  223. print(task_name)
  224. print("=" * 60)
  225. print("💡 交互提示:")
  226. print(" - 执行过程中输入 'p' 或 'pause' 暂停并进入交互模式")
  227. print(" - 执行过程中输入 'q' 或 'quit' 停止执行")
  228. print("=" * 60)
  229. print()
  230. resume_trace_id = args.trace
  231. if resume_trace_id:
  232. existing_trace = await store.get_trace(resume_trace_id)
  233. if not existing_trace:
  234. print(f"\n错误: Trace 不存在: {resume_trace_id}")
  235. sys.exit(1)
  236. final_response = ""
  237. current_trace_id = resume_trace_id
  238. current_sequence = 0
  239. should_exit = False
  240. try:
  241. run_config = copy.deepcopy(RUN_CONFIG)
  242. run_config.model = model
  243. run_config.temperature = float(prompt.config.get("temperature", run_config.temperature))
  244. run_config.max_iterations = int(prompt.config.get("max_iterations", run_config.max_iterations))
  245. if resume_trace_id:
  246. initial_messages = None
  247. run_config.trace_id = resume_trace_id
  248. else:
  249. initial_messages = messages
  250. run_config.name = "热点内容搜索"
  251. while not should_exit:
  252. if current_trace_id:
  253. run_config.trace_id = current_trace_id
  254. final_response = ""
  255. if current_trace_id and initial_messages is None:
  256. check_trace = await store.get_trace(current_trace_id)
  257. if check_trace and check_trace.status in ("completed", "failed"):
  258. if check_trace.status == "completed":
  259. print("\n[Trace] ✅ 已完成")
  260. print(f" - Total cost: ${check_trace.total_cost:.4f}")
  261. else:
  262. print(f"\n[Trace] ❌ 已失败: {check_trace.error_message}")
  263. current_sequence = check_trace.head_sequence
  264. menu_result = await interactive.show_menu(current_trace_id, current_sequence)
  265. if menu_result["action"] == "stop":
  266. break
  267. if menu_result["action"] == "continue":
  268. new_messages = menu_result.get("messages", [])
  269. if new_messages:
  270. initial_messages = new_messages
  271. run_config.after_sequence = menu_result.get("after_sequence")
  272. else:
  273. initial_messages = []
  274. run_config.after_sequence = None
  275. continue
  276. break
  277. initial_messages = []
  278. paused = False
  279. try:
  280. async for item in runner.run(messages=initial_messages, config=run_config):
  281. cmd = interactive.check_stdin()
  282. if cmd == "pause":
  283. print("\n⏸️ 正在暂停执行...")
  284. if current_trace_id:
  285. await runner.stop(current_trace_id)
  286. await asyncio.sleep(0.5)
  287. menu_result = await interactive.show_menu(current_trace_id, current_sequence)
  288. if menu_result["action"] == "stop":
  289. should_exit = True
  290. paused = True
  291. break
  292. if menu_result["action"] == "continue":
  293. new_messages = menu_result.get("messages", [])
  294. if new_messages:
  295. initial_messages = new_messages
  296. after_seq = menu_result.get("after_sequence")
  297. if after_seq is not None:
  298. run_config.after_sequence = after_seq
  299. else:
  300. initial_messages = []
  301. run_config.after_sequence = None
  302. paused = True
  303. break
  304. elif cmd == "quit":
  305. print("\n🛑 用户请求停止...")
  306. if current_trace_id:
  307. await runner.stop(current_trace_id)
  308. should_exit = True
  309. break
  310. if isinstance(item, Trace):
  311. current_trace_id = item.trace_id
  312. if item.status == "completed":
  313. print("\n[Trace] ✅ 完成")
  314. print(f" - Total messages: {item.total_messages}")
  315. print(f" - Total cost: ${item.total_cost:.4f}")
  316. elif item.status == "failed":
  317. print(f"\n[Trace] ❌ 失败: {item.error_message}")
  318. elif item.status == "stopped":
  319. print("\n[Trace] ⏸️ 已停止")
  320. elif isinstance(item, Message):
  321. current_sequence = item.sequence
  322. _print_message_details(item)
  323. if item.role == "assistant":
  324. content = item.content
  325. if isinstance(content, dict):
  326. text = content.get("text", "")
  327. tool_calls = content.get("tool_calls")
  328. if text and not tool_calls:
  329. final_response = text
  330. except Exception as e:
  331. print(f"\n执行出错: {e}")
  332. import traceback
  333. traceback.print_exc()
  334. if paused:
  335. if should_exit:
  336. break
  337. continue
  338. if should_exit:
  339. break
  340. if current_trace_id:
  341. menu_result = await interactive.show_menu(current_trace_id, current_sequence)
  342. if menu_result["action"] == "stop":
  343. break
  344. if menu_result["action"] == "continue":
  345. new_messages = menu_result.get("messages", [])
  346. if new_messages:
  347. initial_messages = new_messages
  348. run_config.after_sequence = menu_result.get("after_sequence")
  349. else:
  350. initial_messages = []
  351. run_config.after_sequence = None
  352. continue
  353. break
  354. except KeyboardInterrupt:
  355. print("\n\n用户中断 (Ctrl+C)")
  356. if current_trace_id:
  357. await runner.stop(current_trace_id)
  358. finally:
  359. if current_trace_id:
  360. try:
  361. html_path = store.base_path / current_trace_id / "messages.html"
  362. await trace_to_html(current_trace_id, html_path, base_path=str(store.base_path))
  363. print(f"\n✓ Messages 可视化已保存: {html_path}")
  364. except Exception as e:
  365. print(f"\n⚠ 生成 HTML 失败: {e}")
  366. if final_response:
  367. print()
  368. print("=" * 60)
  369. print("Agent 响应:")
  370. print("=" * 60)
  371. print(final_response)
  372. print("=" * 60)
  373. print()
  374. output_file = output_dir / "result.txt"
  375. with open(output_file, "w", encoding="utf-8") as f:
  376. f.write(final_response)
  377. print(f"✓ 结果已保存到: {output_file}")
  378. print()
  379. if current_trace_id:
  380. html_path = store.base_path / current_trace_id / "messages.html"
  381. print("=" * 60)
  382. print("可视化:")
  383. print("=" * 60)
  384. print(f"1. 本地 HTML: {html_path}")
  385. print()
  386. print("2. API Server:")
  387. print(" python3 api_server.py")
  388. print(" http://localhost:8000/api/traces")
  389. print()
  390. print(f"3. Trace ID: {current_trace_id}")
  391. print("=" * 60)
  392. if __name__ == "__main__":
  393. asyncio.run(main())