events.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. """执行事件结构:事件字段独立于 HTTP / SSE,编码方式由调用方提供。"""
  2. from __future__ import annotations
  3. from typing import Any, Callable, Generic, Iterable, TypeVar
  4. ExecutionEvent = dict[str, Any]
  5. EventT = TypeVar("EventT")
  6. # 事件类型全集(与 1.5 对齐)
  7. AGENT_START = "agent_start"
  8. THINKING = "thinking"
  9. THINKING_TOKEN = "thinking_token"
  10. EXECUTING = "executing"
  11. TOOL_CALL = "tool_call"
  12. TOOL_RESULT = "tool_result"
  13. UPDATED_TODO_LIST = "updated_todo_list"
  14. TOKEN = "token"
  15. PROGRESS = "progress"
  16. INTERRUPT = "interrupt"
  17. CITATIONS = "citations"
  18. AGENT_DONE = "agent_done"
  19. AGENT_ERROR = "agent_error"
  20. ERROR = "error"
  21. DONE = "done"
  22. ALL_EVENT_TYPES: tuple[str, ...] = (
  23. AGENT_START,
  24. THINKING,
  25. THINKING_TOKEN,
  26. EXECUTING,
  27. TOOL_CALL,
  28. TOOL_RESULT,
  29. UPDATED_TODO_LIST,
  30. TOKEN,
  31. PROGRESS,
  32. INTERRUPT,
  33. CITATIONS,
  34. AGENT_DONE,
  35. AGENT_ERROR,
  36. ERROR,
  37. DONE,
  38. )
  39. # 错误码 → 前端提示文案。前端会用 event.code 查这张表兜底。
  40. ERROR_MESSAGES: dict[str, str] = {
  41. "quota_exceeded": "今日额度已用尽",
  42. "too_many_tasks": "并发任务已达上限,请稍后重试",
  43. "task_timeout": "任务执行超时",
  44. "recursion_limit": "任务步骤过多已中止,请拆分为更小的任务",
  45. "model_error": "模型调用失败",
  46. "internal_error": "服务内部错误",
  47. }
  48. class EventBuilder(Generic[EventT]):
  49. """构造统一的执行事件。
  50. 一次请求一个实例,绑定 ``session_id`` 与 ``trace_id``——这两个字段
  51. 每帧都要带,放在实例里避免每个方法都传一遍。
  52. """
  53. def __init__(self, session_id: str, trace_id: str = "", *, encode: Callable[[ExecutionEvent], EventT]) -> None:
  54. self.encode = encode
  55. self.session_id = session_id
  56. self.trace_id = trace_id
  57. # -------- 底层 --------
  58. def event(self, event_type: str, **fields: Any) -> EventT:
  59. """构造一个事件。``type`` 与 ``session_id`` 由本方法统一注入。"""
  60. payload: dict[str, Any] = {"type": event_type, "session_id": self.session_id}
  61. if self.trace_id:
  62. payload["trace_id"] = self.trace_id
  63. payload.update(fields)
  64. return self.encode(payload)
  65. # -------- 生命周期 --------
  66. def agent_start(self, agent: str, cn_agent: str = "") -> EventT:
  67. return self.event(AGENT_START, agent=agent, cn_agent=cn_agent or agent)
  68. def agent_done(
  69. self, agent: str, cn_agent: str = "", duration_ms: int | None = None, **extra: Any
  70. ) -> EventT:
  71. fields: dict[str, Any] = {"agent": agent, "cn_agent": cn_agent or agent}
  72. if duration_ms is not None:
  73. fields["duration_ms"] = duration_ms
  74. fields.update(extra)
  75. return self.event(AGENT_DONE, **fields)
  76. def agent_error(
  77. self, agent: str, message: str, cn_agent: str = "", duration_ms: int | None = None
  78. ) -> EventT:
  79. fields: dict[str, Any] = {
  80. "agent": agent,
  81. "cn_agent": cn_agent or agent,
  82. "message": message,
  83. }
  84. if duration_ms is not None:
  85. fields["duration_ms"] = duration_ms
  86. return self.event(AGENT_ERROR, **fields)
  87. def done(self, duration_ms: int | None = None, **extra: Any) -> EventT:
  88. fields: dict[str, Any] = {}
  89. if duration_ms is not None:
  90. fields["duration_ms"] = duration_ms
  91. fields.update(extra)
  92. return self.event(DONE, **fields)
  93. def error(
  94. self, message: str, code: str = "internal_error", retry_after: int | None = None
  95. ) -> EventT:
  96. fields: dict[str, Any] = {"message": message, "code": code}
  97. if retry_after is not None:
  98. fields["retry_after"] = retry_after
  99. return self.event(ERROR, **fields)
  100. # -------- 过程 --------
  101. def thinking(self, message: str, cn_agent: str = "") -> EventT:
  102. fields: dict[str, Any] = {"message": message}
  103. if cn_agent:
  104. fields["cn_agent"] = cn_agent
  105. return self.event(THINKING, **fields)
  106. def thinking_token(self, content: str) -> EventT:
  107. return self.event(THINKING_TOKEN, content=content)
  108. def executing(
  109. self,
  110. message: str,
  111. cn_agent: str = "",
  112. tools: Iterable[str] | None = None,
  113. cn_tools: Iterable[str] | None = None,
  114. ) -> EventT:
  115. fields: dict[str, Any] = {"message": message}
  116. if cn_agent:
  117. fields["cn_agent"] = cn_agent
  118. tool_list = list(tools or [])
  119. if tool_list:
  120. fields["tools"] = tool_list
  121. fields["cn_tools"] = list(cn_tools or []) or tool_list
  122. return self.event(EXECUTING, **fields)
  123. def tool_call(
  124. self,
  125. tool: str,
  126. args: Any = None,
  127. cn_tool: str = "",
  128. agent_id: str = "",
  129. tool_call_id: str = "",
  130. ) -> EventT:
  131. """工具调用卡。
  132. ``tool_call_id`` 是 **2.0 的附加字段**(1.5 没有):同一轮里同一个工具
  133. 可能被调多次,只按 ``tool`` 名索引卡片会导致结果挂到错误的卡上。
  134. 附加字段不破坏兼容——旧前端忽略未知字段即可。
  135. """
  136. fields: dict[str, Any] = {"tool": tool, "cn_tool": cn_tool or tool, "args": args}
  137. if tool_call_id:
  138. fields["tool_call_id"] = tool_call_id
  139. if agent_id:
  140. fields["agent_id"] = agent_id
  141. return self.event(TOOL_CALL, **fields)
  142. def tool_result(
  143. self,
  144. tool: str,
  145. content: Any = None,
  146. cn_tool: str = "",
  147. agent_id: str = "",
  148. error: bool = False,
  149. tool_call_id: str = "",
  150. ) -> EventT:
  151. """工具结果卡。``tool_call_id`` 与对应的 tool_call 帧一致。"""
  152. fields: dict[str, Any] = {
  153. "tool": tool,
  154. "cn_tool": cn_tool or tool,
  155. "content": content,
  156. "error": error,
  157. }
  158. if tool_call_id:
  159. fields["tool_call_id"] = tool_call_id
  160. if agent_id:
  161. fields["agent_id"] = agent_id
  162. return self.event(TOOL_RESULT, **fields)
  163. def token(self, content: str) -> EventT:
  164. return self.event(TOKEN, content=content)
  165. def todo_list(self, todos: list[dict[str, Any]]) -> EventT:
  166. return self.event(UPDATED_TODO_LIST, todos=todos)
  167. def progress(self, message: str, **extra: Any) -> EventT:
  168. fields: dict[str, Any] = {"message": message}
  169. fields.update(extra)
  170. return self.event(PROGRESS, **fields)
  171. def citations(self, items: list[dict[str, Any]], model_only: bool = False) -> EventT:
  172. return self.event(CITATIONS, items=items, model_only=model_only)
  173. # -------- 中断 --------
  174. def interrupt_tool_approval(
  175. self,
  176. tool: str,
  177. args: Any = None,
  178. reason: str = "",
  179. side_effect: str = "",
  180. **extra: Any,
  181. ) -> EventT:
  182. """工具审批卡(对应前端 ``showToolApprovalCard``)。"""
  183. payload: dict[str, Any] = {
  184. "tool": tool,
  185. "args": args,
  186. "reason": reason,
  187. "side_effect": side_effect,
  188. }
  189. payload.update(extra)
  190. return self.event(INTERRUPT, tool_approval=payload)
  191. def interrupt_ask_user(
  192. self, questions: list[dict[str, Any]], message: str = ""
  193. ) -> EventT:
  194. """提问卡(对应前端 ``showAskUserCard``)。
  195. 每个问题形如 ``{"question": str, "type": "text"|"multiple_choice",
  196. "choices": [...], "required": bool}``。
  197. """
  198. return self.event(
  199. INTERRUPT,
  200. ask_user={"questions": questions},
  201. message=message or "智能体暂停,等待输入",
  202. )
  203. def interrupt_raw(self, message: str = "", **extra: Any) -> EventT:
  204. """无法归类的中断:只给一条提示,前端显示 toast。"""
  205. return self.event(INTERRUPT, message=message, **extra)