tree_dump.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825
  1. """
  2. Step 树 Debug 输出
  3. 将 Step 树以完整格式输出到文件,便于开发调试。
  4. 使用方式:
  5. 1. 命令行实时查看:
  6. watch -n 0.5 cat .trace/tree.txt
  7. 2. VS Code 打开文件自动刷新:
  8. code .trace/tree.txt
  9. 3. 代码中使用:
  10. from agent.execution import dump_tree
  11. dump_tree(trace, steps)
  12. """
  13. import json
  14. from datetime import datetime
  15. from pathlib import Path
  16. from typing import Any, Dict, List, Optional
  17. # 默认输出路径
  18. DEFAULT_DUMP_PATH = ".trace/tree.txt"
  19. DEFAULT_JSON_PATH = ".trace/tree.json"
  20. DEFAULT_MD_PATH = ".trace/tree.md"
  21. class StepTreeDumper:
  22. """Step 树 Debug 输出器"""
  23. def __init__(self, output_path: str = DEFAULT_DUMP_PATH):
  24. self.output_path = Path(output_path)
  25. self.output_path.parent.mkdir(parents=True, exist_ok=True)
  26. def dump(
  27. self,
  28. trace: Optional[Dict[str, Any]] = None,
  29. steps: Optional[List[Dict[str, Any]]] = None,
  30. title: str = "Step Tree Debug",
  31. ) -> str:
  32. """
  33. 输出完整的树形结构到文件
  34. Args:
  35. trace: Trace 字典(可选)
  36. steps: Step 字典列表
  37. title: 输出标题
  38. Returns:
  39. 输出的文本内容
  40. """
  41. lines = []
  42. # 标题和时间
  43. lines.append("=" * 60)
  44. lines.append(f" {title}")
  45. lines.append(f" Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  46. lines.append("=" * 60)
  47. lines.append("")
  48. # Trace 信息
  49. if trace:
  50. lines.append("## Trace")
  51. lines.append(f" trace_id: {trace.get('trace_id', 'N/A')}")
  52. lines.append(f" task: {trace.get('task', 'N/A')}")
  53. lines.append(f" status: {trace.get('status', 'N/A')}")
  54. lines.append(f" total_steps: {trace.get('total_steps', 0)}")
  55. lines.append(f" total_tokens: {trace.get('total_tokens', 0)}")
  56. lines.append(f" total_cost: {trace.get('total_cost', 0.0):.4f}")
  57. lines.append("")
  58. # 统计摘要
  59. if steps:
  60. lines.append("## Statistics")
  61. stats = self._calculate_statistics(steps)
  62. lines.append(f" Total steps: {stats['total']}")
  63. lines.append(f" By type:")
  64. for step_type, count in sorted(stats['by_type'].items()):
  65. lines.append(f" {step_type}: {count}")
  66. lines.append(f" By status:")
  67. for status, count in sorted(stats['by_status'].items()):
  68. lines.append(f" {status}: {count}")
  69. if stats['total_duration_ms'] > 0:
  70. lines.append(f" Total duration: {stats['total_duration_ms']}ms")
  71. if stats['total_tokens'] > 0:
  72. lines.append(f" Total tokens: {stats['total_tokens']}")
  73. if stats['total_cost'] > 0:
  74. lines.append(f" Total cost: ${stats['total_cost']:.4f}")
  75. lines.append("")
  76. # Step 树
  77. if steps:
  78. lines.append("## Steps")
  79. lines.append("")
  80. # 构建树结构
  81. tree = self._build_tree(steps)
  82. tree_output = self._render_tree(tree, steps)
  83. lines.append(tree_output)
  84. content = "\n".join(lines)
  85. # 写入文件
  86. self.output_path.write_text(content, encoding="utf-8")
  87. return content
  88. def _calculate_statistics(self, steps: List[Dict[str, Any]]) -> Dict[str, Any]:
  89. """计算统计信息"""
  90. stats = {
  91. 'total': len(steps),
  92. 'by_type': {},
  93. 'by_status': {},
  94. 'total_duration_ms': 0,
  95. 'total_tokens': 0,
  96. 'total_cost': 0.0,
  97. }
  98. for step in steps:
  99. # 按类型统计
  100. step_type = step.get('step_type', 'unknown')
  101. stats['by_type'][step_type] = stats['by_type'].get(step_type, 0) + 1
  102. # 按状态统计
  103. status = step.get('status', 'unknown')
  104. stats['by_status'][status] = stats['by_status'].get(status, 0) + 1
  105. # 累计指标
  106. if step.get('duration_ms'):
  107. stats['total_duration_ms'] += step.get('duration_ms', 0)
  108. if step.get('tokens'):
  109. stats['total_tokens'] += step.get('tokens', 0)
  110. if step.get('cost'):
  111. stats['total_cost'] += step.get('cost', 0.0)
  112. return stats
  113. def _build_tree(self, steps: List[Dict[str, Any]]) -> Dict[str, List[str]]:
  114. """构建父子关系映射"""
  115. # parent_id -> [child_ids]
  116. children: Dict[str, List[str]] = {"__root__": []}
  117. for step in steps:
  118. step_id = step.get("step_id", "")
  119. parent_id = step.get("parent_id")
  120. if parent_id is None:
  121. children["__root__"].append(step_id)
  122. else:
  123. if parent_id not in children:
  124. children[parent_id] = []
  125. children[parent_id].append(step_id)
  126. return children
  127. def _render_tree(
  128. self,
  129. tree: Dict[str, List[str]],
  130. steps: List[Dict[str, Any]],
  131. parent_id: str = "__root__",
  132. indent: int = 0,
  133. ) -> str:
  134. """递归渲染树结构"""
  135. # step_id -> step 映射
  136. step_map = {s.get("step_id"): s for s in steps}
  137. lines = []
  138. child_ids = tree.get(parent_id, [])
  139. for i, step_id in enumerate(child_ids):
  140. step = step_map.get(step_id, {})
  141. is_last = i == len(child_ids) - 1
  142. # 渲染当前节点
  143. node_output = self._render_node(step, indent, is_last)
  144. lines.append(node_output)
  145. # 递归渲染子节点
  146. if step_id in tree:
  147. child_output = self._render_tree(tree, steps, step_id, indent + 1)
  148. lines.append(child_output)
  149. return "\n".join(lines)
  150. def _render_node(self, step: Dict[str, Any], indent: int, is_last: bool) -> str:
  151. """渲染单个节点的完整信息"""
  152. lines = []
  153. # 缩进和连接符
  154. prefix = " " * indent
  155. connector = "└── " if is_last else "├── "
  156. child_prefix = " " * indent + (" " if is_last else "│ ")
  157. # 状态图标
  158. status = step.get("status", "unknown")
  159. status_icons = {
  160. "completed": "✓",
  161. "in_progress": "→",
  162. "planned": "○",
  163. "failed": "✗",
  164. "skipped": "⊘",
  165. "awaiting_approval": "⏸",
  166. }
  167. icon = status_icons.get(status, "?")
  168. # 类型和描述
  169. step_type = step.get("step_type", "unknown")
  170. description = step.get("description", "")
  171. # 第一行:类型和描述
  172. lines.append(f"{prefix}{connector}[{icon}] {step_type}: {description}")
  173. # 详细信息
  174. step_id = step.get("step_id", "")[:8] # 只显示前 8 位
  175. lines.append(f"{child_prefix}id: {step_id}...")
  176. # 关键字段:sequence, status, parent_id
  177. sequence = step.get("sequence")
  178. if sequence is not None:
  179. lines.append(f"{child_prefix}sequence: {sequence}")
  180. lines.append(f"{child_prefix}status: {status}")
  181. parent_id = step.get("parent_id")
  182. if parent_id:
  183. lines.append(f"{child_prefix}parent_id: {parent_id[:8]}...")
  184. # 执行指标
  185. if step.get("duration_ms") is not None:
  186. lines.append(f"{child_prefix}duration: {step.get('duration_ms')}ms")
  187. if step.get("tokens") is not None:
  188. lines.append(f"{child_prefix}tokens: {step.get('tokens')}")
  189. if step.get("cost") is not None:
  190. lines.append(f"{child_prefix}cost: ${step.get('cost'):.4f}")
  191. # summary(如果有)
  192. if step.get("summary"):
  193. summary = step.get("summary", "")
  194. # 截断长 summary
  195. if len(summary) > 100:
  196. summary = summary[:100] + "..."
  197. lines.append(f"{child_prefix}summary: {summary}")
  198. # 错误信息(结构化显示)
  199. error = step.get("error")
  200. if error:
  201. lines.append(f"{child_prefix}error:")
  202. lines.append(f"{child_prefix} code: {error.get('code', 'UNKNOWN')}")
  203. error_msg = error.get('message', '')
  204. if len(error_msg) > 200:
  205. error_msg = error_msg[:200] + "..."
  206. lines.append(f"{child_prefix} message: {error_msg}")
  207. lines.append(f"{child_prefix} retryable: {error.get('retryable', True)}")
  208. # data 内容(格式化输出,更激进的截断)
  209. data = step.get("data", {})
  210. if data:
  211. lines.append(f"{child_prefix}data:")
  212. data_lines = self._format_data(data, child_prefix + " ", max_value_len=150)
  213. lines.append(data_lines)
  214. # 时间
  215. created_at = step.get("created_at", "")
  216. if created_at:
  217. if isinstance(created_at, str):
  218. # 只显示时间部分
  219. time_part = created_at.split("T")[-1][:8] if "T" in created_at else created_at
  220. else:
  221. time_part = created_at.strftime("%H:%M:%S")
  222. lines.append(f"{child_prefix}time: {time_part}")
  223. lines.append("") # 空行分隔
  224. return "\n".join(lines)
  225. def _format_data(self, data: Dict[str, Any], prefix: str, max_value_len: int = 150) -> str:
  226. """格式化 data 字典(更激进的截断策略)"""
  227. lines = []
  228. for key, value in data.items():
  229. # 格式化值
  230. if isinstance(value, str):
  231. # 检测图片数据
  232. if value.startswith("data:image") or (len(value) > 10000 and not "\n" in value[:100]):
  233. lines.append(f"{prefix}{key}: [IMAGE_DATA: {len(value)} chars, truncated]")
  234. continue
  235. if len(value) > max_value_len:
  236. value_str = value[:max_value_len] + f"... ({len(value)} chars)"
  237. else:
  238. value_str = value
  239. # 处理多行字符串
  240. if "\n" in value_str:
  241. first_line = value_str.split("\n")[0]
  242. line_count = value.count("\n") + 1
  243. value_str = first_line + f"... ({line_count} lines)"
  244. elif isinstance(value, (dict, list)):
  245. value_str = json.dumps(value, ensure_ascii=False, indent=2)
  246. if len(value_str) > max_value_len:
  247. value_str = value_str[:max_value_len] + "..."
  248. # 缩进多行
  249. value_str = value_str.replace("\n", "\n" + prefix + " ")
  250. else:
  251. value_str = str(value)
  252. lines.append(f"{prefix}{key}: {value_str}")
  253. return "\n".join(lines)
  254. def dump_markdown(
  255. self,
  256. trace: Optional[Dict[str, Any]] = None,
  257. steps: Optional[List[Dict[str, Any]]] = None,
  258. title: str = "Step Tree Debug",
  259. output_path: Optional[str] = None,
  260. ) -> str:
  261. """
  262. 输出 Markdown 格式(支持折叠,完整内容)
  263. Args:
  264. trace: Trace 字典(可选)
  265. steps: Step 字典列表
  266. title: 输出标题
  267. output_path: 输出路径(默认 .trace/tree.md)
  268. Returns:
  269. 输出的 Markdown 内容
  270. """
  271. lines = []
  272. # 标题
  273. lines.append(f"# {title}")
  274. lines.append("")
  275. lines.append(f"*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
  276. lines.append("")
  277. # Trace 信息
  278. if trace:
  279. lines.append("## Trace")
  280. lines.append("")
  281. lines.append(f"- **trace_id**: `{trace.get('trace_id', 'N/A')}`")
  282. lines.append(f"- **task**: {trace.get('task', 'N/A')}")
  283. lines.append(f"- **status**: {trace.get('status', 'N/A')}")
  284. lines.append(f"- **total_steps**: {trace.get('total_steps', 0)}")
  285. lines.append(f"- **total_tokens**: {trace.get('total_tokens', 0)}")
  286. lines.append(f"- **total_cost**: ${trace.get('total_cost', 0.0):.4f}")
  287. lines.append("")
  288. # 统计摘要
  289. if steps:
  290. lines.append("## Statistics")
  291. lines.append("")
  292. stats = self._calculate_statistics(steps)
  293. lines.append(f"- **Total steps**: {stats['total']}")
  294. lines.append("")
  295. lines.append("**By type:**")
  296. lines.append("")
  297. for step_type, count in sorted(stats['by_type'].items()):
  298. lines.append(f"- `{step_type}`: {count}")
  299. lines.append("")
  300. lines.append("**By status:**")
  301. lines.append("")
  302. for status, count in sorted(stats['by_status'].items()):
  303. lines.append(f"- `{status}`: {count}")
  304. lines.append("")
  305. if stats['total_duration_ms'] > 0:
  306. lines.append(f"- **Total duration**: {stats['total_duration_ms']}ms")
  307. if stats['total_tokens'] > 0:
  308. lines.append(f"- **Total tokens**: {stats['total_tokens']}")
  309. if stats['total_cost'] > 0:
  310. lines.append(f"- **Total cost**: ${stats['total_cost']:.4f}")
  311. lines.append("")
  312. # Steps
  313. if steps:
  314. lines.append("## Steps")
  315. lines.append("")
  316. # 构建树并渲染为 Markdown
  317. tree = self._build_tree(steps)
  318. step_map = {s.get("step_id"): s for s in steps}
  319. md_output = self._render_markdown_tree(tree, step_map, level=3)
  320. lines.append(md_output)
  321. content = "\n".join(lines)
  322. # 写入文件
  323. if output_path is None:
  324. output_path = str(self.output_path).replace(".txt", ".md")
  325. Path(output_path).write_text(content, encoding="utf-8")
  326. return content
  327. def _render_markdown_tree(
  328. self,
  329. tree: Dict[str, List[str]],
  330. step_map: Dict[str, Dict[str, Any]],
  331. parent_id: str = "__root__",
  332. level: int = 3,
  333. ) -> str:
  334. """递归渲染 Markdown 树"""
  335. lines = []
  336. child_ids = tree.get(parent_id, [])
  337. for step_id in child_ids:
  338. step = step_map.get(step_id, {})
  339. # 渲染节点
  340. node_md = self._render_markdown_node(step, level)
  341. lines.append(node_md)
  342. # 递归子节点
  343. if step_id in tree:
  344. child_md = self._render_markdown_tree(tree, step_map, step_id, level + 1)
  345. lines.append(child_md)
  346. return "\n".join(lines)
  347. def _render_markdown_node(self, step: Dict[str, Any], level: int) -> str:
  348. """渲染单个节点的 Markdown"""
  349. lines = []
  350. # 标题
  351. status = step.get("status", "unknown")
  352. status_icons = {
  353. "completed": "✓",
  354. "in_progress": "→",
  355. "planned": "○",
  356. "failed": "✗",
  357. "skipped": "⊘",
  358. "awaiting_approval": "⏸",
  359. }
  360. icon = status_icons.get(status, "?")
  361. step_type = step.get("step_type", "unknown")
  362. description = step.get("description", "")
  363. heading = "#" * level
  364. lines.append(f"{heading} [{icon}] {step_type}: {description}")
  365. lines.append("")
  366. # 基本信息
  367. lines.append("**基本信息**")
  368. lines.append("")
  369. step_id = step.get("step_id", "")[:16]
  370. lines.append(f"- **id**: `{step_id}...`")
  371. # 关键字段
  372. sequence = step.get("sequence")
  373. if sequence is not None:
  374. lines.append(f"- **sequence**: {sequence}")
  375. lines.append(f"- **status**: {status}")
  376. parent_id = step.get("parent_id")
  377. if parent_id:
  378. lines.append(f"- **parent_id**: `{parent_id[:16]}...`")
  379. # 执行指标
  380. if step.get("duration_ms") is not None:
  381. lines.append(f"- **duration**: {step.get('duration_ms')}ms")
  382. if step.get("tokens") is not None:
  383. lines.append(f"- **tokens**: {step.get('tokens')}")
  384. if step.get("cost") is not None:
  385. lines.append(f"- **cost**: ${step.get('cost'):.4f}")
  386. created_at = step.get("created_at", "")
  387. if created_at:
  388. if isinstance(created_at, str):
  389. time_part = created_at.split("T")[-1][:8] if "T" in created_at else created_at
  390. else:
  391. time_part = created_at.strftime("%H:%M:%S")
  392. lines.append(f"- **time**: {time_part}")
  393. lines.append("")
  394. # 错误信息
  395. error = step.get("error")
  396. if error:
  397. lines.append("<details>")
  398. lines.append("<summary><b>❌ Error</b></summary>")
  399. lines.append("")
  400. lines.append(f"- **code**: `{error.get('code', 'UNKNOWN')}`")
  401. lines.append(f"- **retryable**: {error.get('retryable', True)}")
  402. lines.append(f"- **message**:")
  403. lines.append("```")
  404. error_msg = error.get('message', '')
  405. if len(error_msg) > 500:
  406. error_msg = error_msg[:500] + "..."
  407. lines.append(error_msg)
  408. lines.append("```")
  409. lines.append("")
  410. lines.append("</details>")
  411. lines.append("")
  412. # Summary
  413. if step.get("summary"):
  414. lines.append("<details>")
  415. lines.append("<summary><b>📝 Summary</b></summary>")
  416. lines.append("")
  417. summary = step.get('summary', '')
  418. if len(summary) > 1000:
  419. summary = summary[:1000] + "..."
  420. lines.append(f"```\n{summary}\n```")
  421. lines.append("")
  422. lines.append("</details>")
  423. lines.append("")
  424. # Data(更激进的截断)
  425. data = step.get("data", {})
  426. if data:
  427. lines.append(self._render_markdown_data(data))
  428. lines.append("")
  429. return "\n".join(lines)
  430. def _render_markdown_data(self, data: Dict[str, Any]) -> str:
  431. """渲染 data 字典为可折叠的 Markdown"""
  432. lines = []
  433. # 定义输出顺序(重要的放前面)
  434. key_order = ["messages", "tools", "response", "content", "tool_calls", "model"]
  435. # 先按顺序输出重要的 key
  436. remaining_keys = set(data.keys())
  437. for key in key_order:
  438. if key in data:
  439. lines.append(self._render_data_item(key, data[key]))
  440. remaining_keys.remove(key)
  441. # 再输出剩余的 key
  442. for key in sorted(remaining_keys):
  443. lines.append(self._render_data_item(key, data[key]))
  444. return "\n".join(lines)
  445. def _render_data_item(self, key: str, value: Any) -> str:
  446. """渲染单个 data 项(更激进的截断)"""
  447. # 确定图标
  448. icon_map = {
  449. "messages": "📨",
  450. "response": "🤖",
  451. "tools": "🛠️",
  452. "tool_calls": "🔧",
  453. "model": "🎯",
  454. "error": "❌",
  455. "content": "💬",
  456. "output": "📤",
  457. "arguments": "⚙️",
  458. }
  459. icon = icon_map.get(key, "📄")
  460. # 特殊处理:跳过 None 值
  461. if value is None:
  462. return ""
  463. # 特殊处理 messages 中的图片引用
  464. if key == 'messages' and isinstance(value, list):
  465. # 统计图片数量
  466. image_count = 0
  467. for msg in value:
  468. if isinstance(msg, dict):
  469. content = msg.get('content', [])
  470. if isinstance(content, list):
  471. for item in content:
  472. if isinstance(item, dict) and item.get('type') == 'image_url':
  473. url = item.get('image_url', {}).get('url', '')
  474. if url.startswith('blob://'):
  475. image_count += 1
  476. if image_count > 0:
  477. # 显示图片摘要
  478. lines = []
  479. lines.append("<details>")
  480. lines.append(f"<summary><b>📨 Messages (含 {image_count} 张图片)</b></summary>")
  481. lines.append("")
  482. lines.append("```json")
  483. # 渲染消息,图片显示为简化格式
  484. simplified_messages = []
  485. for msg in value:
  486. if isinstance(msg, dict):
  487. simplified_msg = msg.copy()
  488. content = msg.get('content', [])
  489. if isinstance(content, list):
  490. new_content = []
  491. for item in content:
  492. if isinstance(item, dict) and item.get('type') == 'image_url':
  493. url = item.get('image_url', {}).get('url', '')
  494. if url.startswith('blob://'):
  495. blob_ref = url.replace('blob://', '')
  496. size = item.get('image_url', {}).get('size', 0)
  497. size_kb = size / 1024 if size > 0 else 0
  498. new_content.append({
  499. 'type': 'image_url',
  500. 'image_url': {
  501. 'url': f'[IMAGE: {blob_ref[:8]}... ({size_kb:.1f}KB)]'
  502. }
  503. })
  504. else:
  505. new_content.append(item)
  506. else:
  507. new_content.append(item)
  508. simplified_msg['content'] = new_content
  509. simplified_messages.append(simplified_msg)
  510. else:
  511. simplified_messages.append(msg)
  512. lines.append(json.dumps(simplified_messages, ensure_ascii=False, indent=2))
  513. lines.append("```")
  514. lines.append("")
  515. lines.append("</details>")
  516. return "\n".join(lines)
  517. # 判断是否需要折叠(长内容或复杂结构)
  518. needs_collapse = False
  519. if isinstance(value, str):
  520. needs_collapse = len(value) > 100 or "\n" in value
  521. elif isinstance(value, (dict, list)):
  522. needs_collapse = True
  523. if needs_collapse:
  524. lines = []
  525. # 可折叠块
  526. lines.append("<details>")
  527. lines.append(f"<summary><b>{icon} {key.capitalize()}</b></summary>")
  528. lines.append("")
  529. # 格式化内容(更激进的截断)
  530. if isinstance(value, str):
  531. # 检查是否包含图片 base64
  532. if "data:image" in value or (isinstance(value, str) and len(value) > 10000 and not "\n" in value[:100]):
  533. lines.append("```")
  534. lines.append(f"[IMAGE DATA: {len(value)} chars, truncated for display]")
  535. lines.append("```")
  536. elif len(value) > 2000:
  537. # 超长文本,只显示前500字符
  538. lines.append("```")
  539. lines.append(value[:500])
  540. lines.append(f"... (truncated, total {len(value)} chars)")
  541. lines.append("```")
  542. else:
  543. lines.append("```")
  544. lines.append(value)
  545. lines.append("```")
  546. elif isinstance(value, (dict, list)):
  547. # 递归截断图片 base64
  548. truncated_value = self._truncate_image_data(value)
  549. json_str = json.dumps(truncated_value, ensure_ascii=False, indent=2)
  550. # 如果 JSON 太长,也截断
  551. if len(json_str) > 3000:
  552. json_str = json_str[:3000] + "\n... (truncated)"
  553. lines.append("```json")
  554. lines.append(json_str)
  555. lines.append("```")
  556. lines.append("")
  557. lines.append("</details>")
  558. return "\n".join(lines)
  559. else:
  560. # 简单值,直接显示
  561. return f"- **{icon} {key}**: `{value}`"
  562. def _truncate_image_data(self, obj: Any, max_length: int = 200) -> Any:
  563. """递归截断对象中的图片 base64 数据"""
  564. if isinstance(obj, dict):
  565. result = {}
  566. for key, value in obj.items():
  567. # 检测图片 URL(data:image/...;base64,...)
  568. if isinstance(value, str) and value.startswith("data:image"):
  569. # 提取 MIME 类型和数据长度
  570. header_end = value.find(",")
  571. if header_end > 0:
  572. header = value[:header_end]
  573. data = value[header_end+1:]
  574. data_size_kb = len(data) / 1024
  575. result[key] = f"<IMAGE_DATA: {data_size_kb:.1f}KB, {header}, preview: {data[:50]}...>"
  576. else:
  577. result[key] = value[:max_length] + f"... ({len(value)} chars)"
  578. # 检测 blob 引用
  579. elif isinstance(value, str) and value.startswith("blob://"):
  580. blob_ref = value.replace("blob://", "")
  581. result[key] = f"<BLOB_REF: {blob_ref[:8]}...>"
  582. else:
  583. result[key] = self._truncate_image_data(value, max_length)
  584. return result
  585. elif isinstance(obj, list):
  586. return [self._truncate_image_data(item, max_length) for item in obj]
  587. elif isinstance(obj, str) and len(obj) > 100000:
  588. # 超长字符串(可能是未检测到的 base64)
  589. return obj[:max_length] + f"... (TRUNCATED: {len(obj)} chars total)"
  590. else:
  591. return obj
  592. def dump_tree(
  593. trace: Optional[Any] = None,
  594. steps: Optional[List[Any]] = None,
  595. output_path: str = DEFAULT_DUMP_PATH,
  596. title: str = "Step Tree Debug",
  597. ) -> str:
  598. """
  599. 便捷函数:输出 Step 树到文件
  600. Args:
  601. trace: Trace 对象或字典
  602. steps: Step 对象或字典列表
  603. output_path: 输出文件路径
  604. title: 输出标题
  605. Returns:
  606. 输出的文本内容
  607. 示例:
  608. from agent.debug import dump_tree
  609. # 每次 step 变化后调用
  610. dump_tree(trace, steps)
  611. # 自定义路径
  612. dump_tree(trace, steps, output_path=".debug/my_trace.txt")
  613. """
  614. # 转换为字典
  615. trace_dict = None
  616. if trace is not None:
  617. trace_dict = trace.to_dict() if hasattr(trace, "to_dict") else trace
  618. steps_list = []
  619. if steps:
  620. for step in steps:
  621. if hasattr(step, "to_dict"):
  622. steps_list.append(step.to_dict())
  623. else:
  624. steps_list.append(step)
  625. dumper = StepTreeDumper(output_path)
  626. return dumper.dump(trace_dict, steps_list, title)
  627. def dump_json(
  628. trace: Optional[Any] = None,
  629. steps: Optional[List[Any]] = None,
  630. output_path: str = DEFAULT_JSON_PATH,
  631. ) -> str:
  632. """
  633. 输出完整的 JSON 格式(用于程序化分析)
  634. Args:
  635. trace: Trace 对象或字典
  636. steps: Step 对象或字典列表
  637. output_path: 输出文件路径
  638. Returns:
  639. JSON 字符串
  640. """
  641. path = Path(output_path)
  642. path.parent.mkdir(parents=True, exist_ok=True)
  643. # 转换为字典
  644. trace_dict = None
  645. if trace is not None:
  646. trace_dict = trace.to_dict() if hasattr(trace, "to_dict") else trace
  647. steps_list = []
  648. if steps:
  649. for step in steps:
  650. if hasattr(step, "to_dict"):
  651. steps_list.append(step.to_dict())
  652. else:
  653. steps_list.append(step)
  654. data = {
  655. "generated_at": datetime.now().isoformat(),
  656. "trace": trace_dict,
  657. "steps": steps_list,
  658. }
  659. content = json.dumps(data, ensure_ascii=False, indent=2)
  660. path.write_text(content, encoding="utf-8")
  661. return content
  662. def dump_markdown(
  663. trace: Optional[Any] = None,
  664. steps: Optional[List[Any]] = None,
  665. output_path: str = DEFAULT_MD_PATH,
  666. title: str = "Step Tree Debug",
  667. ) -> str:
  668. """
  669. 便捷函数:输出 Markdown 格式(支持折叠,完整内容)
  670. Args:
  671. trace: Trace 对象或字典
  672. steps: Step 对象或字典列表
  673. output_path: 输出文件路径(默认 .trace/tree.md)
  674. title: 输出标题
  675. Returns:
  676. 输出的 Markdown 内容
  677. 示例:
  678. from agent.debug import dump_markdown
  679. # 输出完整可折叠的 Markdown
  680. dump_markdown(trace, steps)
  681. # 自定义路径
  682. dump_markdown(trace, steps, output_path=".debug/debug.md")
  683. """
  684. # 转换为字典
  685. trace_dict = None
  686. if trace is not None:
  687. trace_dict = trace.to_dict() if hasattr(trace, "to_dict") else trace
  688. steps_list = []
  689. if steps:
  690. for step in steps:
  691. if hasattr(step, "to_dict"):
  692. steps_list.append(step.to_dict())
  693. else:
  694. steps_list.append(step)
  695. dumper = StepTreeDumper(output_path)
  696. return dumper.dump_markdown(trace_dict, steps_list, title, output_path)