api.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. """
  2. Step 树 RESTful API
  3. 提供 Trace 和 Step 的查询接口,支持懒加载
  4. """
  5. from typing import List, Optional, Dict, Any
  6. from fastapi import APIRouter, HTTPException, Query
  7. from pydantic import BaseModel
  8. from agent.trace.protocols import TraceStore
  9. router = APIRouter(prefix="/api/traces", tags=["traces"])
  10. # ===== Response 模型 =====
  11. class TraceListResponse(BaseModel):
  12. """Trace 列表响应"""
  13. traces: List[Dict[str, Any]]
  14. class TraceResponse(BaseModel):
  15. """Trace 元数据响应"""
  16. trace_id: str
  17. mode: str
  18. task: Optional[str] = None
  19. agent_type: Optional[str] = None
  20. status: str
  21. total_steps: int
  22. total_tokens: int
  23. total_cost: float
  24. created_at: str
  25. completed_at: Optional[str] = None
  26. class StepNode(BaseModel):
  27. """Step 节点(递归结构)"""
  28. step_id: str
  29. step_type: str
  30. status: str
  31. description: str
  32. sequence: int
  33. parent_id: Optional[str] = None
  34. data: Optional[Dict[str, Any]] = None
  35. summary: Optional[str] = None
  36. duration_ms: Optional[int] = None
  37. tokens: Optional[int] = None
  38. cost: Optional[float] = None
  39. created_at: str
  40. children: List["StepNode"] = []
  41. class TreeResponse(BaseModel):
  42. """完整树响应"""
  43. trace_id: str
  44. root_steps: List[StepNode]
  45. class NodeResponse(BaseModel):
  46. """节点响应"""
  47. step_id: Optional[str]
  48. step_type: Optional[str]
  49. description: Optional[str]
  50. children: List[StepNode]
  51. # ===== 全局 TraceStore(由 api_server.py 注入)=====
  52. _trace_store: Optional[TraceStore] = None
  53. def set_trace_store(store: TraceStore):
  54. """设置 TraceStore 实例"""
  55. global _trace_store
  56. _trace_store = store
  57. def get_trace_store() -> TraceStore:
  58. """获取 TraceStore 实例"""
  59. if _trace_store is None:
  60. raise RuntimeError("TraceStore not initialized")
  61. return _trace_store
  62. # ===== 路由 =====
  63. @router.get("", response_model=TraceListResponse)
  64. async def list_traces(
  65. mode: Optional[str] = None,
  66. agent_type: Optional[str] = None,
  67. uid: Optional[str] = None,
  68. status: Optional[str] = None,
  69. limit: int = Query(20, le=100)
  70. ):
  71. """
  72. 列出 Traces
  73. Args:
  74. mode: 模式过滤(call/agent)
  75. agent_type: Agent 类型过滤
  76. uid: 用户 ID 过滤
  77. status: 状态过滤(running/completed/failed)
  78. limit: 最大返回数量
  79. """
  80. store = get_trace_store()
  81. traces = await store.list_traces(
  82. mode=mode,
  83. agent_type=agent_type,
  84. uid=uid,
  85. status=status,
  86. limit=limit
  87. )
  88. return TraceListResponse(
  89. traces=[t.to_dict() for t in traces]
  90. )
  91. @router.get("/{trace_id}", response_model=TraceResponse)
  92. async def get_trace(trace_id: str):
  93. """
  94. 获取 Trace 元数据
  95. Args:
  96. trace_id: Trace ID
  97. """
  98. store = get_trace_store()
  99. trace = await store.get_trace(trace_id)
  100. if not trace:
  101. raise HTTPException(status_code=404, detail="Trace not found")
  102. return TraceResponse(**trace.to_dict())
  103. @router.get("/{trace_id}/tree", response_model=TreeResponse)
  104. async def get_full_tree(trace_id: str):
  105. """
  106. 获取完整 Step 树(小型 Trace 推荐)
  107. Args:
  108. trace_id: Trace ID
  109. """
  110. store = get_trace_store()
  111. # 验证 Trace 存在
  112. trace = await store.get_trace(trace_id)
  113. if not trace:
  114. raise HTTPException(status_code=404, detail="Trace not found")
  115. # 获取所有 Steps
  116. steps = await store.get_trace_steps(trace_id)
  117. # 构建树结构
  118. root_nodes = await _build_tree(store, trace_id, None, expand=True, max_depth=999)
  119. return TreeResponse(
  120. trace_id=trace_id,
  121. root_steps=root_nodes
  122. )
  123. @router.get("/{trace_id}/node/{step_id}", response_model=NodeResponse)
  124. async def get_node(
  125. trace_id: str,
  126. step_id: str,
  127. expand: bool = Query(False, description="是否加载子节点"),
  128. max_depth: int = Query(1, ge=1, le=10, description="递归深度")
  129. ):
  130. """
  131. 懒加载节点 + 子节点(大型 Trace 推荐)
  132. Args:
  133. trace_id: Trace ID
  134. step_id: Step ID("null" 表示根节点)
  135. expand: 是否加载子节点
  136. max_depth: 递归深度
  137. """
  138. store = get_trace_store()
  139. # 验证 Trace 存在
  140. trace = await store.get_trace(trace_id)
  141. if not trace:
  142. raise HTTPException(status_code=404, detail="Trace not found")
  143. # step_id = "null" 表示根节点
  144. actual_step_id = None if step_id == "null" else step_id
  145. # 验证 Step 存在(非根节点)
  146. if actual_step_id:
  147. step = await store.get_step(actual_step_id)
  148. if not step or step.trace_id != trace_id:
  149. raise HTTPException(status_code=404, detail="Step not found")
  150. # 构建节点树
  151. children = await _build_tree(store, trace_id, actual_step_id, expand, max_depth)
  152. # 如果是根节点,返回所有根 Steps
  153. if actual_step_id is None:
  154. return NodeResponse(
  155. step_id=None,
  156. step_type=None,
  157. description=None,
  158. children=children
  159. )
  160. # 否则返回当前节点 + 子节点
  161. step = await store.get_step(actual_step_id)
  162. return NodeResponse(
  163. step_id=step.step_id,
  164. step_type=step.step_type,
  165. description=step.description,
  166. children=children
  167. )
  168. # ===== 核心算法:懒加载树构建 =====
  169. async def _build_tree(
  170. store: TraceStore,
  171. trace_id: str,
  172. step_id: Optional[str],
  173. expand: bool = False,
  174. max_depth: int = 1,
  175. current_depth: int = 0
  176. ) -> List[StepNode]:
  177. """
  178. 懒加载核心逻辑(简洁版本)
  179. 没有"批次计算"、没有"同层完整性检查"
  180. 只有简单的递归遍历
  181. Args:
  182. store: TraceStore 实例
  183. trace_id: Trace ID
  184. step_id: 当前 Step ID(None 表示根节点)
  185. expand: 是否展开子节点
  186. max_depth: 最大递归深度
  187. current_depth: 当前递归深度
  188. Returns:
  189. List[StepNode]: 节点列表
  190. """
  191. # 1. 获取当前层节点
  192. if step_id is None:
  193. # 根节点:获取所有 parent_id=None 的 Steps
  194. steps = await store.get_trace_steps(trace_id)
  195. current_nodes = [s for s in steps if s.parent_id is None]
  196. else:
  197. # 非根节点:获取子节点
  198. current_nodes = await store.get_step_children(step_id)
  199. # 2. 构建响应
  200. result_nodes = []
  201. for step in current_nodes:
  202. node_dict = step.to_dict()
  203. node_dict["children"] = []
  204. # 3. 递归加载子节点(可选)
  205. if expand and current_depth < max_depth:
  206. children = await store.get_step_children(step.step_id)
  207. if children:
  208. node_dict["children"] = await _build_tree(
  209. store, trace_id, step.step_id,
  210. expand=True, max_depth=max_depth,
  211. current_depth=current_depth + 1
  212. )
  213. result_nodes.append(StepNode(**node_dict))
  214. return result_nodes