run.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. """
  2. 示例(增强版)
  3. 使用 Agent 模式 + Skills
  4. 新增功能:
  5. 1. 支持命令行随时打断(输入 'p' 暂停,'q' 退出)
  6. 2. 暂停后可插入干预消息
  7. 3. 支持触发经验总结
  8. 4. 查看当前 GoalTree
  9. 5. 框架层自动清理不完整的工具调用
  10. 6. 支持通过 --trace <ID> 恢复已有 Trace 继续执行
  11. """
  12. import argparse
  13. import os
  14. import sys
  15. import select
  16. import asyncio
  17. import json
  18. from pathlib import Path
  19. from typing import Any
  20. # Clash Verge TUN 模式兼容:禁止 httpx/urllib 自动检测系统 HTTP 代理
  21. # TUN 虚拟网卡已在网络层接管所有流量,不需要应用层再走 HTTP 代理,
  22. # 否则 httpx 检测到 macOS 系统代理 (127.0.0.1:7897) 会导致 ConnectError
  23. # os.environ.setdefault("no_proxy", "*")
  24. # 添加项目根目录到 Python 路径
  25. sys.path.insert(0, str(Path(__file__).parent.parent.parent))
  26. from dotenv import load_dotenv
  27. load_dotenv()
  28. from agent.llm.prompts import SimplePrompt
  29. from agent.core.runner import AgentRunner, RunConfig
  30. from agent.core.presets import AgentPreset, register_preset
  31. from agent.trace import (
  32. FileSystemTraceStore,
  33. Trace,
  34. Message,
  35. )
  36. from agent.llm import create_openrouter_llm_call
  37. from agent.tools import get_tool_registry
  38. DEFAULT_MODEL = "anthropic/claude-sonnet-4.5"
  39. # DEFAULT_MODEL = "google/gemini-3-flash-preview"
  40. # ===== 非阻塞 stdin 检测 =====
  41. if sys.platform == 'win32':
  42. import msvcrt
  43. def check_stdin() -> str | None:
  44. """
  45. 跨平台非阻塞检查 stdin 输入。
  46. Windows: 使用 msvcrt.kbhit()
  47. macOS/Linux: 使用 select.select()
  48. """
  49. if sys.platform == 'win32':
  50. # 检查是否有按键按下
  51. if msvcrt.kbhit():
  52. # 读取按下的字符(msvcrt.getwch 是非阻塞读取宽字符)
  53. ch = msvcrt.getwch().lower()
  54. if ch == 'p':
  55. return 'pause'
  56. if ch == 'q':
  57. return 'quit'
  58. # 如果是其他按键,可以选择消耗掉或者忽略
  59. return None
  60. else:
  61. # Unix/Mac 逻辑
  62. ready, _, _ = select.select([sys.stdin], [], [], 0)
  63. if ready:
  64. line = sys.stdin.readline().strip().lower()
  65. if line in ('p', 'pause'):
  66. return 'pause'
  67. if line in ('q', 'quit'):
  68. return 'quit'
  69. return None
  70. # ===== 格式化打印 =====
  71. def _format_json(obj: Any, indent: int = 2) -> str:
  72. """格式化 JSON 对象为字符串"""
  73. try:
  74. return json.dumps(obj, indent=indent, ensure_ascii=False)
  75. except (TypeError, ValueError):
  76. # 如果无法序列化为 JSON,返回字符串表示
  77. return str(obj)
  78. def _print_message_details(message: Message):
  79. """完整打印消息的详细信息"""
  80. print("\n" + "=" * 80)
  81. print(f"[Message #{message.sequence}] {message.role.upper()}")
  82. print("=" * 80)
  83. # 基本信息
  84. if message.goal_id:
  85. print(f"Goal ID: {message.goal_id}")
  86. if message.parent_sequence is not None:
  87. print(f"Parent Sequence: {message.parent_sequence}")
  88. if message.tool_call_id:
  89. print(f"Tool Call ID: {message.tool_call_id}")
  90. # 内容打印
  91. if message.role == "user":
  92. print("\n[输入内容]")
  93. print("-" * 80)
  94. if isinstance(message.content, str):
  95. print(message.content)
  96. else:
  97. print(_format_json(message.content))
  98. elif message.role == "assistant":
  99. content = message.content
  100. if isinstance(content, dict):
  101. text = content.get("text", "")
  102. tool_calls = content.get("tool_calls")
  103. if text:
  104. print("\n[LLM 文本回复]")
  105. print("-" * 80)
  106. print(text)
  107. if tool_calls:
  108. print(f"\n[工具调用] (共 {len(tool_calls)} 个)")
  109. print("-" * 80)
  110. for idx, tc in enumerate(tool_calls, 1):
  111. func = tc.get("function", {})
  112. tool_name = func.get("name", "unknown")
  113. tool_id = tc.get("id", "unknown")
  114. arguments = func.get("arguments", {})
  115. print(f"\n工具 #{idx}: {tool_name}")
  116. print(f" Call ID: {tool_id}")
  117. print(f" 参数:")
  118. # 尝试解析 arguments(可能是字符串或字典)
  119. if isinstance(arguments, str):
  120. try:
  121. parsed_args = json.loads(arguments)
  122. print(_format_json(parsed_args, indent=4))
  123. except json.JSONDecodeError:
  124. print(f" {arguments}")
  125. else:
  126. print(_format_json(arguments, indent=4))
  127. elif isinstance(content, str):
  128. print("\n[LLM 文本回复]")
  129. print("-" * 80)
  130. print(content)
  131. else:
  132. print("\n[内容]")
  133. print("-" * 80)
  134. print(_format_json(content))
  135. if message.finish_reason:
  136. print(f"\n完成原因: {message.finish_reason}")
  137. elif message.role == "tool":
  138. content = message.content
  139. print("\n[工具执行结果]")
  140. print("-" * 80)
  141. if isinstance(content, dict):
  142. tool_name = content.get("tool_name", "unknown")
  143. result = content.get("result", content)
  144. print(f"工具名称: {tool_name}")
  145. print(f"\n返回结果:")
  146. if isinstance(result, str):
  147. print(result)
  148. elif isinstance(result, list):
  149. # 可能是多模态内容(包含图片)
  150. for idx, item in enumerate(result, 1):
  151. if isinstance(item, dict) and item.get("type") == "image_url":
  152. print(f" [{idx}] 图片 (base64, 已省略显示)")
  153. else:
  154. print(f" [{idx}] {item}")
  155. else:
  156. print(_format_json(result))
  157. else:
  158. print(str(content) if content is not None else "(无内容)")
  159. elif message.role == "system":
  160. print("\n[系统提示]")
  161. print("-" * 80)
  162. if isinstance(message.content, str):
  163. print(message.content)
  164. else:
  165. print(_format_json(message.content))
  166. # Token 和成本信息
  167. if message.prompt_tokens is not None or message.completion_tokens is not None:
  168. print("\n[Token 使用]")
  169. print("-" * 80)
  170. if message.prompt_tokens is not None:
  171. print(f" 输入 Tokens: {message.prompt_tokens:,}")
  172. if message.completion_tokens is not None:
  173. print(f" 输出 Tokens: {message.completion_tokens:,}")
  174. if message.reasoning_tokens is not None:
  175. print(f" 推理 Tokens: {message.reasoning_tokens:,}")
  176. if message.cache_creation_tokens is not None:
  177. print(f" 缓存创建 Tokens: {message.cache_creation_tokens:,}")
  178. if message.cache_read_tokens is not None:
  179. print(f" 缓存读取 Tokens: {message.cache_read_tokens:,}")
  180. if message.tokens:
  181. print(f" 总计 Tokens: {message.tokens:,}")
  182. if message.cost is not None:
  183. print(f"\n[成本] ${message.cost:.6f}")
  184. if message.duration_ms is not None:
  185. print(f"[执行时间] {message.duration_ms}ms")
  186. print("=" * 80 + "\n")
  187. # ===== 交互菜单 =====
  188. def _read_multiline() -> str:
  189. """
  190. 读取多行输入,以连续两次回车(空行)结束。
  191. 单次回车只是换行,不会提前终止输入。
  192. """
  193. print("\n请输入干预消息(连续输入两次回车结束):")
  194. lines: list[str] = []
  195. blank_count = 0
  196. while True:
  197. line = input()
  198. if line == "":
  199. blank_count += 1
  200. if blank_count >= 2:
  201. break
  202. lines.append("") # 保留单个空行
  203. else:
  204. blank_count = 0
  205. lines.append(line)
  206. # 去掉尾部多余空行
  207. while lines and lines[-1] == "":
  208. lines.pop()
  209. return "\n".join(lines)
  210. async def show_interactive_menu(
  211. runner: AgentRunner,
  212. trace_id: str,
  213. current_sequence: int,
  214. store: FileSystemTraceStore,
  215. ):
  216. """
  217. 显示交互式菜单,让用户选择操作。
  218. 进入本函数前不再有后台线程占用 stdin,所以 input() 能正常工作。
  219. """
  220. print("\n" + "=" * 60)
  221. print(" 执行已暂停")
  222. print("=" * 60)
  223. print("请选择操作:")
  224. print(" 1. 插入干预消息并继续")
  225. print(" 2. 触发经验总结(reflect)")
  226. print(" 3. 查看当前 GoalTree")
  227. print(" 4. 手动压缩上下文(compact)")
  228. print(" 5. 继续执行")
  229. print(" 6. 停止执行")
  230. print("=" * 60)
  231. while True:
  232. choice = input("请输入选项 (1-6): ").strip()
  233. if choice == "1":
  234. text = _read_multiline()
  235. if not text:
  236. print("未输入任何内容,取消操作")
  237. continue
  238. print(f"\n将插入干预消息并继续执行...")
  239. # 从 store 读取实际的 last_sequence,避免本地 current_sequence 过时
  240. live_trace = await store.get_trace(trace_id)
  241. actual_sequence = live_trace.last_sequence if live_trace and live_trace.last_sequence else current_sequence
  242. return {
  243. "action": "continue",
  244. "messages": [{"role": "user", "content": text}],
  245. "after_sequence": actual_sequence,
  246. }
  247. elif choice == "2":
  248. # 触发经验总结
  249. print("\n触发经验总结...")
  250. focus = input("请输入反思重点(可选,直接回车跳过): ").strip()
  251. from agent.trace.compaction import build_reflect_prompt
  252. # 保存当前 head_sequence
  253. trace = await store.get_trace(trace_id)
  254. saved_head = trace.head_sequence
  255. prompt = build_reflect_prompt()
  256. if focus:
  257. prompt += f"\n\n请特别关注:{focus}"
  258. print("正在生成反思...")
  259. reflect_cfg = RunConfig(trace_id=trace_id, max_iterations=1, tools=[])
  260. reflection_text = ""
  261. try:
  262. result = await runner.run_result(
  263. messages=[{"role": "user", "content": prompt}],
  264. config=reflect_cfg,
  265. )
  266. reflection_text = result.get("summary", "")
  267. finally:
  268. # 恢复 head_sequence(反思消息成为侧枝)
  269. await store.update_trace(trace_id, head_sequence=saved_head)
  270. # 追加到 experiences 文件
  271. if reflection_text:
  272. from datetime import datetime
  273. experiences_path = runner.experiences_path or "./.cache/experiences.md"
  274. os.makedirs(os.path.dirname(experiences_path), exist_ok=True)
  275. header = f"\n\n---\n\n## {trace_id} ({datetime.now().strftime('%Y-%m-%d %H:%M')})\n\n"
  276. with open(experiences_path, "a", encoding="utf-8") as f:
  277. f.write(header + reflection_text + "\n")
  278. print(f"\n反思已保存到: {experiences_path}")
  279. print("\n--- 反思内容 ---")
  280. print(reflection_text)
  281. print("--- 结束 ---\n")
  282. else:
  283. print("未生成反思内容")
  284. continue
  285. elif choice == "3":
  286. goal_tree = await store.get_goal_tree(trace_id)
  287. if goal_tree and goal_tree.goals:
  288. print("\n当前 GoalTree:")
  289. print(goal_tree.to_prompt())
  290. else:
  291. print("\n当前没有 Goal")
  292. continue
  293. elif choice == "4":
  294. # 手动压缩上下文
  295. print("\n正在执行上下文压缩(compact)...")
  296. try:
  297. goal_tree = await store.get_goal_tree(trace_id)
  298. trace = await store.get_trace(trace_id)
  299. if not trace:
  300. print("未找到 Trace,无法压缩")
  301. continue
  302. # 重建当前 history
  303. main_path = await store.get_main_path_messages(trace_id, trace.head_sequence)
  304. history = [msg.to_llm_dict() for msg in main_path]
  305. head_seq = main_path[-1].sequence if main_path else 0
  306. next_seq = head_seq + 1
  307. compact_config = RunConfig(trace_id=trace_id)
  308. new_history, new_head, new_seq = await runner._compress_history(
  309. trace_id=trace_id,
  310. history=history,
  311. goal_tree=goal_tree,
  312. config=compact_config,
  313. sequence=next_seq,
  314. head_seq=head_seq,
  315. )
  316. print(f"\n✅ 压缩完成: {len(history)} 条消息 → {len(new_history)} 条")
  317. except Exception as e:
  318. print(f"\n❌ 压缩失败: {e}")
  319. continue
  320. elif choice == "5":
  321. print("\n继续执行...")
  322. return {"action": "continue"}
  323. elif choice == "6":
  324. print("\n停止执行...")
  325. return {"action": "stop"}
  326. else:
  327. print("无效选项,请重新输入")
  328. async def main():
  329. # 解析命令行参数
  330. parser = argparse.ArgumentParser(description="任务 (Agent 模式 + 交互增强)")
  331. parser.add_argument(
  332. "--trace", type=str, default=None,
  333. help="已有的 Trace ID,用于恢复继续执行(不指定则新建)",
  334. )
  335. args = parser.parse_args()
  336. # 路径配置
  337. base_dir = Path(__file__).parent
  338. project_root = base_dir.parent.parent
  339. prompt_path = base_dir / "create.prompt"
  340. output_dir = base_dir / "output_1"
  341. output_dir.mkdir(exist_ok=True)
  342. # 加载项目级 presets(examples/create/presets.json)
  343. presets_path = base_dir / "presets.json"
  344. if presets_path.exists():
  345. import json
  346. with open(presets_path, "r", encoding="utf-8") as f:
  347. project_presets = json.load(f)
  348. for name, cfg in project_presets.items():
  349. register_preset(name, AgentPreset(**cfg))
  350. print(f" - 已加载项目 presets: {list(project_presets.keys())}")
  351. # Skills 目录(可选:用户自定义 skills)
  352. # 注意:内置 skills(agent/memory/skills/)会自动加载
  353. skills_dir = str(base_dir / "skills")
  354. print("=" * 60)
  355. print("mcp/skills 发现、获取、评价 分析任务 (Agent 模式 + 交互增强)")
  356. print("=" * 60)
  357. print()
  358. print("💡 交互提示:")
  359. print(" - 执行过程中输入 'p' 或 'pause' 暂停并进入交互模式")
  360. print(" - 执行过程中输入 'q' 或 'quit' 停止执行")
  361. print("=" * 60)
  362. print()
  363. # 1. 加载 prompt
  364. print("1. 加载 prompt 配置...")
  365. prompt = SimplePrompt(prompt_path)
  366. # 读取 system.md 并替换 {system} 占位符
  367. system_md_path = base_dir / "PRD" / "system.md"
  368. if system_md_path.exists():
  369. system_content = system_md_path.read_text(encoding='utf-8')
  370. if 'system' in prompt._messages and '{system}' in prompt._messages['system']:
  371. prompt._messages['system'] = prompt._messages['system'].replace('{system}', system_content)
  372. else:
  373. print(f" - 警告: system.md 文件不存在: {system_md_path}")
  374. # 读取 create_process.md 并替换 {create_process} 占位符
  375. create_process_md_path = base_dir / "PRD" / "create_process.md"
  376. if create_process_md_path.exists():
  377. create_process_content = create_process_md_path.read_text(encoding='utf-8')
  378. if 'system' in prompt._messages and '{create_process}' in prompt._messages['system']:
  379. prompt._messages['system'] = prompt._messages['system'].replace('{create_process}', create_process_content)
  380. print(f" - 已替换 create_process.md 内容到 prompt")
  381. else:
  382. print(f" - 警告: prompt 中未找到 {{create_process}} 占位符")
  383. else:
  384. print(f" - 警告: create_process.md 文件不存在: {create_process_md_path}")
  385. # 读取 user.md 并替换 {user} 占位符
  386. user_md_path = base_dir / "PRD" / "user.md"
  387. if user_md_path.exists():
  388. user_content = user_md_path.read_text(encoding='utf-8')
  389. if 'user' in prompt._messages and '{user}' in prompt._messages['user']:
  390. prompt._messages['user'] = prompt._messages['user'].replace('{user}', user_content)
  391. print(f" - 已替换 user.md 内容到 prompt")
  392. else:
  393. print(f" - 警告: prompt 中未找到 {{user}} 占位符")
  394. else:
  395. print(f" - 警告: user.md 文件不存在: {user_md_path}")
  396. print("\n替换后的prompt:")
  397. print("=" * 60)
  398. print("System:")
  399. print("-" * 60)
  400. print(prompt._messages.get('system', ''))
  401. print("=" * 60)
  402. if 'user' in prompt._messages:
  403. print("\nUser:")
  404. print("-" * 60)
  405. print(prompt._messages['user'])
  406. print("=" * 60)
  407. print()
  408. # 2. 构建消息(仅新建时使用,恢复时消息已在 trace 中)
  409. print("2. 构建任务消息...")
  410. messages = prompt.build_messages()
  411. # 3. 创建 Agent Runner(配置 skills)
  412. print("3. 创建 Agent Runner...")
  413. print(f" - Skills 目录: {skills_dir}")
  414. print(f" - 模型: {prompt.config.get('model', 'sonnet-4.5')}")
  415. # 加载自定义工具
  416. print(" - 加载自定义工具: topic_search")
  417. import examples.create.tool # 选题检索工具,用于在数据库中匹配已有帖子选题
  418. store = FileSystemTraceStore(base_path=".trace")
  419. runner = AgentRunner(
  420. trace_store=store,
  421. llm_call=create_openrouter_llm_call(model=prompt.config.get('model', DEFAULT_MODEL)),
  422. skills_dir=skills_dir,
  423. experiences_path="./.cache/experiences.md",
  424. debug=True
  425. )
  426. # 4. 判断是新建还是恢复
  427. resume_trace_id = args.trace
  428. if resume_trace_id:
  429. # 验证 trace 存在
  430. existing_trace = await store.get_trace(resume_trace_id)
  431. if not existing_trace:
  432. print(f"\n错误: Trace 不存在: {resume_trace_id}")
  433. sys.exit(1)
  434. print(f"4. 恢复已有 Trace: {resume_trace_id[:8]}...")
  435. print(f" - 状态: {existing_trace.status}")
  436. print(f" - 消息数: {existing_trace.total_messages}")
  437. print(f" - 任务: {existing_trace.task}")
  438. else:
  439. print(f"4. 启动新 Agent 模式...")
  440. print()
  441. final_response = ""
  442. current_trace_id = resume_trace_id
  443. current_sequence = 0
  444. should_exit = False
  445. try:
  446. # 恢复模式:不发送初始消息,只指定 trace_id 续跑
  447. if resume_trace_id:
  448. initial_messages = None # None = 未设置,触发早期菜单检查
  449. config = RunConfig(
  450. model=prompt.config.get('model', DEFAULT_MODEL),
  451. temperature=float(prompt.config.get('temperature', 0.3)),
  452. max_iterations=1000,
  453. trace_id=resume_trace_id,
  454. )
  455. else:
  456. initial_messages = messages
  457. config = RunConfig(
  458. model=prompt.config.get('model', DEFAULT_MODEL),
  459. temperature=float(prompt.config.get('temperature', 0.3)),
  460. max_iterations=1000,
  461. name="社交媒体内容解构、建构、评估任务",
  462. )
  463. while not should_exit:
  464. # 如果是续跑,需要指定 trace_id
  465. if current_trace_id:
  466. config.trace_id = current_trace_id
  467. # 清理上一轮的响应,避免失败后显示旧内容
  468. final_response = ""
  469. # 如果 trace 已完成/失败且没有新消息,直接进入交互菜单
  470. # 注意:initial_messages 为 None 表示未设置(首次加载),[] 表示有意为空(用户选择"继续")
  471. if current_trace_id and initial_messages is None:
  472. check_trace = await store.get_trace(current_trace_id)
  473. if check_trace and check_trace.status in ("completed", "failed"):
  474. if check_trace.status == "completed":
  475. print(f"\n[Trace] ✅ 已完成")
  476. print(f" - Total messages: {check_trace.total_messages}")
  477. print(f" - Total cost: ${check_trace.total_cost:.4f}")
  478. else:
  479. print(f"\n[Trace] ❌ 已失败: {check_trace.error_message}")
  480. current_sequence = check_trace.head_sequence
  481. menu_result = await show_interactive_menu(
  482. runner, current_trace_id, current_sequence, store
  483. )
  484. if menu_result["action"] == "stop":
  485. break
  486. elif menu_result["action"] == "continue":
  487. new_messages = menu_result.get("messages", [])
  488. if new_messages:
  489. initial_messages = new_messages
  490. config.after_sequence = menu_result.get("after_sequence")
  491. else:
  492. # 无新消息:对 failed trace 意味着重试,对 completed 意味着继续
  493. initial_messages = []
  494. config.after_sequence = None
  495. continue
  496. break
  497. # 对 stopped/running 等非终态的 trace,直接续跑
  498. initial_messages = []
  499. print(f"{'▶️ 开始执行...' if not current_trace_id else '▶️ 继续执行...'}")
  500. # 执行 Agent
  501. paused = False
  502. try:
  503. async for item in runner.run(messages=initial_messages, config=config):
  504. # 检查用户中断
  505. cmd = check_stdin()
  506. if cmd == 'pause':
  507. # 暂停执行
  508. print("\n⏸️ 正在暂停执行...")
  509. if current_trace_id:
  510. await runner.stop(current_trace_id)
  511. # 等待一小段时间让 runner 处理 stop 信号
  512. await asyncio.sleep(0.5)
  513. # 显示交互菜单
  514. menu_result = await show_interactive_menu(
  515. runner, current_trace_id, current_sequence, store
  516. )
  517. if menu_result["action"] == "stop":
  518. should_exit = True
  519. paused = True
  520. break
  521. elif menu_result["action"] == "continue":
  522. # 检查是否有新消息需要插入
  523. new_messages = menu_result.get("messages", [])
  524. if new_messages:
  525. # 有干预消息,需要重新启动循环
  526. initial_messages = new_messages
  527. after_seq = menu_result.get("after_sequence")
  528. if after_seq is not None:
  529. config.after_sequence = after_seq
  530. paused = True
  531. break
  532. else:
  533. # 没有新消息,需要重启执行
  534. initial_messages = []
  535. config.after_sequence = None
  536. paused = True
  537. break
  538. elif cmd == 'quit':
  539. print("\n🛑 用户请求停止...")
  540. if current_trace_id:
  541. await runner.stop(current_trace_id)
  542. should_exit = True
  543. break
  544. # 处理 Trace 对象(整体状态变化)
  545. if isinstance(item, Trace):
  546. current_trace_id = item.trace_id
  547. if item.status == "running":
  548. print(f"[Trace] 开始: {item.trace_id[:8]}...")
  549. elif item.status == "completed":
  550. print(f"\n[Trace] ✅ 完成")
  551. print(f" - Total messages: {item.total_messages}")
  552. print(f" - Total tokens: {item.total_tokens}")
  553. print(f" - Total cost: ${item.total_cost:.4f}")
  554. elif item.status == "failed":
  555. print(f"\n[Trace] ❌ 失败: {item.error_message}")
  556. elif item.status == "stopped":
  557. print(f"\n[Trace] ⏸️ 已停止")
  558. # 处理 Message 对象(执行过程)
  559. elif isinstance(item, Message):
  560. current_sequence = item.sequence
  561. # 完整打印所有消息详情
  562. _print_message_details(item)
  563. # 保留原有的简化输出逻辑(用于最终响应)
  564. if item.role == "assistant":
  565. content = item.content
  566. if isinstance(content, dict):
  567. text = content.get("text", "")
  568. tool_calls = content.get("tool_calls")
  569. if text and not tool_calls:
  570. # 纯文本回复(最终响应)
  571. final_response = text
  572. except Exception as e:
  573. print(f"\n执行出错: {e}")
  574. import traceback
  575. traceback.print_exc()
  576. # paused → 菜单已在暂停时内联显示过
  577. if paused:
  578. if should_exit:
  579. break
  580. continue
  581. # quit → 直接退出
  582. if should_exit:
  583. break
  584. # Runner 退出(完成/失败/停止/异常)→ 显示交互菜单
  585. if current_trace_id:
  586. menu_result = await show_interactive_menu(
  587. runner, current_trace_id, current_sequence, store
  588. )
  589. if menu_result["action"] == "stop":
  590. break
  591. elif menu_result["action"] == "continue":
  592. new_messages = menu_result.get("messages", [])
  593. if new_messages:
  594. initial_messages = new_messages
  595. config.after_sequence = menu_result.get("after_sequence")
  596. else:
  597. initial_messages = []
  598. config.after_sequence = None
  599. continue
  600. break
  601. except KeyboardInterrupt:
  602. print("\n\n用户中断 (Ctrl+C)")
  603. if current_trace_id:
  604. await runner.stop(current_trace_id)
  605. # 6. 输出结果
  606. if final_response:
  607. print()
  608. print("=" * 60)
  609. print("Agent 响应:")
  610. print("=" * 60)
  611. print(final_response)
  612. print("=" * 60)
  613. print()
  614. # 7. 保存结果
  615. output_file = output_dir / "result.txt"
  616. with open(output_file, 'w', encoding='utf-8') as f:
  617. f.write(final_response)
  618. print(f"✓ 结果已保存到: {output_file}")
  619. print()
  620. # 可视化提示
  621. if current_trace_id:
  622. print("=" * 60)
  623. print("可视化 Step Tree:")
  624. print("=" * 60)
  625. print("1. 启动 API Server:")
  626. print(" python3 api_server.py")
  627. print()
  628. print("2. 浏览器访问:")
  629. print(" http://localhost:8000/api/traces")
  630. print()
  631. print(f"3. Trace ID: {current_trace_id}")
  632. print("=" * 60)
  633. if __name__ == "__main__":
  634. asyncio.run(main())