visualize_trace.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. """
  2. Trace 树可视化工具
  3. 读取 trace 目录并生成树形结构的可视化输出
  4. """
  5. import json
  6. import sys
  7. from pathlib import Path
  8. from datetime import datetime
  9. def load_trace_meta(trace_dir):
  10. """加载 trace 的 meta.json"""
  11. meta_file = trace_dir / "meta.json"
  12. if not meta_file.exists():
  13. return None
  14. with open(meta_file, 'r', encoding='utf-8') as f:
  15. return json.load(f)
  16. def format_duration(start_str, end_str):
  17. """计算并格式化持续时间"""
  18. if not start_str or not end_str:
  19. return "N/A"
  20. try:
  21. start = datetime.fromisoformat(start_str)
  22. end = datetime.fromisoformat(end_str)
  23. duration = (end - start).total_seconds()
  24. return f"{duration:.1f}s"
  25. except:
  26. return "N/A"
  27. def extract_mode_from_trace_id(trace_id):
  28. """从 trace_id 中提取模式"""
  29. if '@delegate-' in trace_id:
  30. return 'delegate'
  31. elif '@explore-' in trace_id:
  32. return 'explore'
  33. elif '@evaluate-' in trace_id:
  34. return 'evaluate'
  35. return 'main'
  36. def print_trace_tree(trace_base_path, output_file=None):
  37. """打印 trace 树结构"""
  38. trace_base = Path(trace_base_path)
  39. if not trace_base.exists():
  40. print(f"错误: Trace 目录不存在: {trace_base}")
  41. return
  42. # 查找所有 trace 目录
  43. all_traces = {}
  44. main_trace_id = None
  45. for trace_dir in sorted(trace_base.iterdir()):
  46. if not trace_dir.is_dir():
  47. continue
  48. meta = load_trace_meta(trace_dir)
  49. if not meta:
  50. continue
  51. trace_id = meta['trace_id']
  52. all_traces[trace_id] = {
  53. 'meta': meta,
  54. 'dir': trace_dir,
  55. 'children': []
  56. }
  57. # 找到主 trace
  58. if meta.get('parent_trace_id') is None:
  59. main_trace_id = trace_id
  60. if not main_trace_id:
  61. print("错误: 未找到主 trace")
  62. return
  63. # 构建树结构
  64. for trace_id, trace_info in all_traces.items():
  65. parent_id = trace_info['meta'].get('parent_trace_id')
  66. if parent_id and parent_id in all_traces:
  67. all_traces[parent_id]['children'].append(trace_id)
  68. # 输出函数
  69. def output(text):
  70. print(text)
  71. if output_file:
  72. output_file.write(text + '\n')
  73. # 打印树
  74. output("=" * 80)
  75. output("Trace 执行树")
  76. output("=" * 80)
  77. output("")
  78. def print_node(trace_id, prefix="", is_last=True):
  79. trace_info = all_traces[trace_id]
  80. meta = trace_info['meta']
  81. # 树形连接符
  82. connector = "└── " if is_last else "├── "
  83. # 提取信息
  84. mode = extract_mode_from_trace_id(trace_id)
  85. task = meta.get('task', 'N/A')
  86. if len(task) > 60:
  87. task = task[:60] + "..."
  88. status = meta.get('status', 'unknown')
  89. messages = meta.get('total_messages', 0)
  90. tokens = meta.get('total_tokens', 0)
  91. duration = format_duration(
  92. meta.get('created_at'),
  93. meta.get('completed_at')
  94. )
  95. # 状态符号
  96. status_symbol = {
  97. 'completed': '✓',
  98. 'failed': '✗',
  99. 'running': '⟳',
  100. }.get(status, '?')
  101. # 打印节点
  102. output(f"{prefix}{connector}[{mode}] {status_symbol} {trace_id[:8]}")
  103. output(f"{prefix}{' ' if is_last else '│ '}Task: {task}")
  104. output(f"{prefix}{' ' if is_last else '│ '}Stats: {messages} msgs, {tokens:,} tokens, {duration}")
  105. # 打印子节点
  106. children = trace_info['children']
  107. for i, child_id in enumerate(children):
  108. is_last_child = (i == len(children) - 1)
  109. child_prefix = prefix + (" " if is_last else "│ ")
  110. print_node(child_id, child_prefix, is_last_child)
  111. # 从主 trace 开始打印
  112. print_node(main_trace_id)
  113. output("")
  114. output("=" * 80)
  115. output("统计信息")
  116. output("=" * 80)
  117. # 统计各模式的数量
  118. mode_counts = {}
  119. total_messages = 0
  120. total_tokens = 0
  121. for trace_info in all_traces.values():
  122. meta = trace_info['meta']
  123. mode = extract_mode_from_trace_id(meta['trace_id'])
  124. mode_counts[mode] = mode_counts.get(mode, 0) + 1
  125. total_messages += meta.get('total_messages', 0)
  126. total_tokens += meta.get('total_tokens', 0)
  127. output(f"总 Trace 数: {len(all_traces)}")
  128. output(f" - main: {mode_counts.get('main', 0)}")
  129. output(f" - delegate: {mode_counts.get('delegate', 0)}")
  130. output(f" - explore: {mode_counts.get('explore', 0)}")
  131. output(f" - evaluate: {mode_counts.get('evaluate', 0)}")
  132. output(f"")
  133. output(f"总消息数: {total_messages}")
  134. output(f"总 Token 数: {total_tokens:,}")
  135. output("=" * 80)
  136. if __name__ == "__main__":
  137. if len(sys.argv) < 2:
  138. print("用法: python visualize_trace.py <trace_directory> [output_file]")
  139. print("示例: python visualize_trace.py .trace")
  140. sys.exit(1)
  141. trace_dir = sys.argv[1]
  142. output_path = sys.argv[2] if len(sys.argv) > 2 else None
  143. output_file = None
  144. if output_path:
  145. output_file = open(output_path, 'w', encoding='utf-8')
  146. try:
  147. print_trace_tree(trace_dir, output_file)
  148. finally:
  149. if output_file:
  150. output_file.close()
  151. print(f"\n✓ 输出已保存到: {output_path}")