tree_dump.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  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.debug 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. class StepTreeDumper:
  21. """Step 树 Debug 输出器"""
  22. def __init__(self, output_path: str = DEFAULT_DUMP_PATH):
  23. self.output_path = Path(output_path)
  24. self.output_path.parent.mkdir(parents=True, exist_ok=True)
  25. def dump(
  26. self,
  27. trace: Optional[Dict[str, Any]] = None,
  28. steps: Optional[List[Dict[str, Any]]] = None,
  29. title: str = "Step Tree Debug",
  30. ) -> str:
  31. """
  32. 输出完整的树形结构到文件
  33. Args:
  34. trace: Trace 字典(可选)
  35. steps: Step 字典列表
  36. title: 输出标题
  37. Returns:
  38. 输出的文本内容
  39. """
  40. lines = []
  41. # 标题和时间
  42. lines.append("=" * 60)
  43. lines.append(f" {title}")
  44. lines.append(f" Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  45. lines.append("=" * 60)
  46. lines.append("")
  47. # Trace 信息
  48. if trace:
  49. lines.append("## Trace")
  50. lines.append(f" trace_id: {trace.get('trace_id', 'N/A')}")
  51. lines.append(f" task: {trace.get('task', 'N/A')}")
  52. lines.append(f" status: {trace.get('status', 'N/A')}")
  53. lines.append(f" total_steps: {trace.get('total_steps', 0)}")
  54. lines.append(f" total_tokens: {trace.get('total_tokens', 0)}")
  55. lines.append(f" total_cost: {trace.get('total_cost', 0.0):.4f}")
  56. lines.append("")
  57. # Step 树
  58. if steps:
  59. lines.append("## Steps")
  60. lines.append("")
  61. # 构建树结构
  62. tree = self._build_tree(steps)
  63. tree_output = self._render_tree(tree, steps)
  64. lines.append(tree_output)
  65. content = "\n".join(lines)
  66. # 写入文件
  67. self.output_path.write_text(content, encoding="utf-8")
  68. return content
  69. def _build_tree(self, steps: List[Dict[str, Any]]) -> Dict[str, List[str]]:
  70. """构建父子关系映射"""
  71. # parent_id -> [child_ids]
  72. children: Dict[str, List[str]] = {"__root__": []}
  73. for step in steps:
  74. step_id = step.get("step_id", "")
  75. parent_id = step.get("parent_id")
  76. if parent_id is None:
  77. children["__root__"].append(step_id)
  78. else:
  79. if parent_id not in children:
  80. children[parent_id] = []
  81. children[parent_id].append(step_id)
  82. return children
  83. def _render_tree(
  84. self,
  85. tree: Dict[str, List[str]],
  86. steps: List[Dict[str, Any]],
  87. parent_id: str = "__root__",
  88. indent: int = 0,
  89. ) -> str:
  90. """递归渲染树结构"""
  91. # step_id -> step 映射
  92. step_map = {s.get("step_id"): s for s in steps}
  93. lines = []
  94. child_ids = tree.get(parent_id, [])
  95. for i, step_id in enumerate(child_ids):
  96. step = step_map.get(step_id, {})
  97. is_last = i == len(child_ids) - 1
  98. # 渲染当前节点
  99. node_output = self._render_node(step, indent, is_last)
  100. lines.append(node_output)
  101. # 递归渲染子节点
  102. if step_id in tree:
  103. child_output = self._render_tree(tree, steps, step_id, indent + 1)
  104. lines.append(child_output)
  105. return "\n".join(lines)
  106. def _render_node(self, step: Dict[str, Any], indent: int, is_last: bool) -> str:
  107. """渲染单个节点的完整信息"""
  108. lines = []
  109. # 缩进和连接符
  110. prefix = " " * indent
  111. connector = "└── " if is_last else "├── "
  112. child_prefix = " " * indent + (" " if is_last else "│ ")
  113. # 状态图标
  114. status = step.get("status", "unknown")
  115. status_icons = {
  116. "completed": "✓",
  117. "in_progress": "→",
  118. "planned": "○",
  119. "failed": "✗",
  120. "skipped": "⊘",
  121. }
  122. icon = status_icons.get(status, "?")
  123. # 类型和描述
  124. step_type = step.get("step_type", "unknown")
  125. description = step.get("description", "")
  126. # 第一行:类型和描述
  127. lines.append(f"{prefix}{connector}[{icon}] {step_type}: {description}")
  128. # 详细信息
  129. step_id = step.get("step_id", "")[:8] # 只显示前 8 位
  130. lines.append(f"{child_prefix}id: {step_id}...")
  131. # 执行指标
  132. if step.get("duration_ms") is not None:
  133. lines.append(f"{child_prefix}duration: {step.get('duration_ms')}ms")
  134. if step.get("tokens") is not None:
  135. lines.append(f"{child_prefix}tokens: {step.get('tokens')}")
  136. if step.get("cost") is not None:
  137. lines.append(f"{child_prefix}cost: ${step.get('cost'):.4f}")
  138. # summary(如果有)
  139. if step.get("summary"):
  140. summary = step.get("summary", "")
  141. # 截断长 summary
  142. if len(summary) > 100:
  143. summary = summary[:100] + "..."
  144. lines.append(f"{child_prefix}summary: {summary}")
  145. # data 内容(格式化输出)
  146. data = step.get("data", {})
  147. if data:
  148. lines.append(f"{child_prefix}data:")
  149. data_lines = self._format_data(data, child_prefix + " ")
  150. lines.append(data_lines)
  151. # 时间
  152. created_at = step.get("created_at", "")
  153. if created_at:
  154. if isinstance(created_at, str):
  155. # 只显示时间部分
  156. time_part = created_at.split("T")[-1][:8] if "T" in created_at else created_at
  157. else:
  158. time_part = created_at.strftime("%H:%M:%S")
  159. lines.append(f"{child_prefix}time: {time_part}")
  160. lines.append("") # 空行分隔
  161. return "\n".join(lines)
  162. def _format_data(self, data: Dict[str, Any], prefix: str, max_value_len: int = 200) -> str:
  163. """格式化 data 字典"""
  164. lines = []
  165. for key, value in data.items():
  166. # 格式化值
  167. if isinstance(value, str):
  168. if len(value) > max_value_len:
  169. value_str = value[:max_value_len] + f"... ({len(value)} chars)"
  170. else:
  171. value_str = value
  172. # 处理多行字符串
  173. if "\n" in value_str:
  174. first_line = value_str.split("\n")[0]
  175. value_str = first_line + f"... ({value_str.count(chr(10))+1} lines)"
  176. elif isinstance(value, (dict, list)):
  177. value_str = json.dumps(value, ensure_ascii=False, indent=2)
  178. if len(value_str) > max_value_len:
  179. value_str = value_str[:max_value_len] + "..."
  180. # 缩进多行
  181. value_str = value_str.replace("\n", "\n" + prefix + " ")
  182. else:
  183. value_str = str(value)
  184. lines.append(f"{prefix}{key}: {value_str}")
  185. return "\n".join(lines)
  186. def dump_tree(
  187. trace: Optional[Any] = None,
  188. steps: Optional[List[Any]] = None,
  189. output_path: str = DEFAULT_DUMP_PATH,
  190. title: str = "Step Tree Debug",
  191. ) -> str:
  192. """
  193. 便捷函数:输出 Step 树到文件
  194. Args:
  195. trace: Trace 对象或字典
  196. steps: Step 对象或字典列表
  197. output_path: 输出文件路径
  198. title: 输出标题
  199. Returns:
  200. 输出的文本内容
  201. 示例:
  202. from agent.debug import dump_tree
  203. # 每次 step 变化后调用
  204. dump_tree(trace, steps)
  205. # 自定义路径
  206. dump_tree(trace, steps, output_path=".debug/my_trace.txt")
  207. """
  208. # 转换为字典
  209. trace_dict = None
  210. if trace is not None:
  211. trace_dict = trace.to_dict() if hasattr(trace, "to_dict") else trace
  212. steps_list = []
  213. if steps:
  214. for step in steps:
  215. if hasattr(step, "to_dict"):
  216. steps_list.append(step.to_dict())
  217. else:
  218. steps_list.append(step)
  219. dumper = StepTreeDumper(output_path)
  220. return dumper.dump(trace_dict, steps_list, title)
  221. def dump_json(
  222. trace: Optional[Any] = None,
  223. steps: Optional[List[Any]] = None,
  224. output_path: str = DEFAULT_JSON_PATH,
  225. ) -> str:
  226. """
  227. 输出完整的 JSON 格式(用于程序化分析)
  228. Args:
  229. trace: Trace 对象或字典
  230. steps: Step 对象或字典列表
  231. output_path: 输出文件路径
  232. Returns:
  233. JSON 字符串
  234. """
  235. path = Path(output_path)
  236. path.parent.mkdir(parents=True, exist_ok=True)
  237. # 转换为字典
  238. trace_dict = None
  239. if trace is not None:
  240. trace_dict = trace.to_dict() if hasattr(trace, "to_dict") else trace
  241. steps_list = []
  242. if steps:
  243. for step in steps:
  244. if hasattr(step, "to_dict"):
  245. steps_list.append(step.to_dict())
  246. else:
  247. steps_list.append(step)
  248. data = {
  249. "generated_at": datetime.now().isoformat(),
  250. "trace": trace_dict,
  251. "steps": steps_list,
  252. }
  253. content = json.dumps(data, ensure_ascii=False, indent=2)
  254. path.write_text(content, encoding="utf-8")
  255. return content