run.py 30 KB

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