event_agent.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import asyncio
  2. import json
  3. from collections.abc import AsyncIterator, Iterable, Sequence
  4. from dataclasses import dataclass
  5. from typing import Any, Protocol
  6. from agent_lab.application.contracts import AgentParams
  7. from agent_lab.application.tools import (
  8. ToolExecutionContext,
  9. ToolRegistry,
  10. build_default_tool_registry,
  11. )
  12. from agent_lab.domain.events import ToolCallEvent
  13. from agent_lab.domain.messages import ChatMessage, StreamItem
  14. class EventAgentChatClient(Protocol):
  15. async def stream_chat(
  16. self,
  17. messages: list[ChatMessage],
  18. tools: list[dict[str, Any]],
  19. params: AgentParams,
  20. ) -> AsyncIterator[StreamItem]:
  21. ...
  22. @dataclass(frozen=True)
  23. class EventAgentRequest:
  24. events: list[ToolCallEvent]
  25. history: list[ChatMessage]
  26. system_prompt: str = ""
  27. extra_body: dict[str, Any] | None = None
  28. session_id: str | None = None
  29. turn_index: int | None = None
  30. round_index: int | None = None
  31. turn_started_at: float | None = None
  32. class EventAgent:
  33. def __init__(
  34. self,
  35. enabled_tools: Iterable[str],
  36. registry: ToolRegistry | None = None,
  37. chat_client: EventAgentChatClient | None = None,
  38. params: AgentParams | None = None,
  39. ) -> None:
  40. self.enabled_tools = set(enabled_tools)
  41. self.registry = registry or build_default_tool_registry()
  42. self.chat_client = chat_client
  43. self.params = params or AgentParams()
  44. self._raw_chunks_by_event: dict[str, list[dict[str, Any]]] = {}
  45. async def handle(
  46. self,
  47. event: ToolCallEvent,
  48. history: Sequence[ChatMessage] = (),
  49. system_prompt: str = "",
  50. extra_body: dict[str, Any] | None = None,
  51. ) -> ChatMessage:
  52. self._raw_chunks_by_event[event.id] = []
  53. if event.name not in self.enabled_tools:
  54. return self._tool_reply(
  55. event,
  56. {
  57. "tool": event.name,
  58. "error": "tool disabled",
  59. },
  60. )
  61. try:
  62. if self.chat_client is None:
  63. payload = self.registry.handle(
  64. event,
  65. context=ToolExecutionContext(
  66. history=history,
  67. system_prompt=system_prompt,
  68. extra_body=extra_body or {},
  69. ),
  70. )
  71. else:
  72. resolved_event = await self._resolve_event_with_llm(
  73. event,
  74. history,
  75. system_prompt,
  76. extra_body,
  77. )
  78. if resolved_event is None:
  79. payload = self.registry.handle(
  80. event,
  81. context=ToolExecutionContext(
  82. history=history,
  83. system_prompt=system_prompt,
  84. extra_body=extra_body or {},
  85. ),
  86. )
  87. else:
  88. payload = self.registry.execute(resolved_event)
  89. except Exception as exc:
  90. payload = {
  91. "tool": event.name,
  92. "error": f"tool handler failed: {exc}",
  93. }
  94. return self._tool_reply(event, payload)
  95. async def handle_many(
  96. self,
  97. events: Sequence[ToolCallEvent],
  98. history: Sequence[ChatMessage],
  99. system_prompt: str = "",
  100. extra_body: dict[str, Any] | None = None,
  101. ) -> list[ChatMessage]:
  102. for event in events:
  103. self._raw_chunks_by_event[event.id] = []
  104. return await asyncio.gather(
  105. *[
  106. self.handle(
  107. event,
  108. history=history,
  109. system_prompt=system_prompt,
  110. extra_body=extra_body,
  111. )
  112. for event in events
  113. ]
  114. )
  115. def raw_model_chunks(
  116. self,
  117. events: Sequence[ToolCallEvent],
  118. ) -> list[dict[str, Any]]:
  119. return [
  120. {
  121. "event_id": event.id,
  122. "event_name": event.name,
  123. "chunks": self._raw_chunks_by_event.get(event.id, []),
  124. }
  125. for event in events
  126. ]
  127. async def _resolve_event_with_llm(
  128. self,
  129. event: ToolCallEvent,
  130. history: Sequence[ChatMessage],
  131. system_prompt: str,
  132. extra_body: dict[str, Any] | None,
  133. ) -> ToolCallEvent | None:
  134. assert self.chat_client is not None
  135. tool = self.registry.tool_schema(event.name)
  136. if tool is None:
  137. return event
  138. messages = self._build_argument_messages(event, history, system_prompt)
  139. params = self.params.model_copy(
  140. update={
  141. "extra_body": (
  142. extra_body if extra_body is not None else self.params.extra_body
  143. ),
  144. }
  145. )
  146. raw_chunks: list[dict[str, Any]] = []
  147. async for item in self.chat_client.stream_chat(
  148. messages=messages,
  149. tools=[tool],
  150. params=params,
  151. ):
  152. if item.kind == "raw_chunk" and item.raw_chunk is not None:
  153. raw_chunks.append(item.raw_chunk)
  154. continue
  155. if (
  156. item.kind in {"provider_tool_call", "event"}
  157. and item.event is not None
  158. ):
  159. self._raw_chunks_by_event[event.id] = raw_chunks
  160. return event.model_copy(
  161. update={
  162. "arguments": item.event.arguments,
  163. "raw_arguments": item.event.raw_arguments,
  164. }
  165. )
  166. self._raw_chunks_by_event[event.id] = raw_chunks
  167. return None
  168. def _build_argument_messages(
  169. self,
  170. event: ToolCallEvent,
  171. history: Sequence[ChatMessage],
  172. system_prompt: str,
  173. ) -> list[ChatMessage]:
  174. messages = [
  175. ChatMessage(
  176. role="system",
  177. content=(
  178. "Generate parameters for the requested EventAgent tool. "
  179. "Call exactly the provided tool with valid JSON arguments."
  180. ),
  181. )
  182. ]
  183. if system_prompt.strip():
  184. messages.append(ChatMessage(role="system", content=system_prompt))
  185. messages.extend(
  186. message
  187. for message in history
  188. if message.role in {"system", "user", "assistant"}
  189. )
  190. messages.append(
  191. ChatMessage(
  192. role="user",
  193. content=f"Resolve arguments for event `{event.name}`.",
  194. )
  195. )
  196. return messages
  197. def summarize_replies(self, replies: Sequence[ChatMessage]) -> ChatMessage | None:
  198. if not replies:
  199. return None
  200. return ChatMessage(
  201. role="user",
  202. content=(
  203. "EventAgent results:\n"
  204. + "\n".join(reply.content for reply in replies)
  205. ),
  206. name="event_agent",
  207. )
  208. def _tool_reply(self, event: ToolCallEvent, payload: dict[str, Any]) -> ChatMessage:
  209. return ChatMessage(
  210. role="tool",
  211. content=json.dumps(payload, ensure_ascii=False),
  212. name=event.name,
  213. tool_call_id=event.id,
  214. )