You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

coding_pub_sub.py 9.4 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. """
  2. This example shows how to use pub/sub to implement
  3. a simple interaction between a tool executor agent and a tool use agent.
  4. 1. The tool use agent receives a user message, and makes an inference using a model.
  5. If the response is a list of function calls, the agent publishes the function calls
  6. to the tool executor agent.
  7. 2. The tool executor agent receives the function calls, executes the tools, and publishes
  8. the results back to the tool use agent.
  9. 3. The tool use agent receives the tool results, and makes an inference using the model again.
  10. 4. The process continues until the inference response is not a list of function calls.
  11. 5. The tool use agent publishes a final response to the user.
  12. """
  13. import asyncio
  14. import json
  15. import os
  16. import sys
  17. import uuid
  18. from dataclasses import dataclass
  19. from typing import Dict, List
  20. from agnext.application import SingleThreadedAgentRuntime
  21. from agnext.components import DefaultTopicId, FunctionCall, RoutedAgent, message_handler
  22. from agnext.components._type_subscription import TypeSubscription
  23. from agnext.components.code_executor import LocalCommandLineCodeExecutor
  24. from agnext.components.models import (
  25. AssistantMessage,
  26. ChatCompletionClient,
  27. FunctionExecutionResult,
  28. FunctionExecutionResultMessage,
  29. LLMMessage,
  30. SystemMessage,
  31. UserMessage,
  32. )
  33. from agnext.components.tools import PythonCodeExecutionTool, Tool
  34. from agnext.core import TopicId
  35. sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
  36. from agnext.core import MessageContext
  37. from common.utils import get_chat_completion_client_from_envs
  38. @dataclass
  39. class ToolExecutionTask:
  40. session_id: str
  41. function_call: FunctionCall
  42. @dataclass
  43. class ToolExecutionTaskResult:
  44. session_id: str
  45. result: FunctionExecutionResult
  46. @dataclass
  47. class UserRequest:
  48. content: str
  49. @dataclass
  50. class AgentResponse:
  51. content: str
  52. class ToolExecutorAgent(RoutedAgent):
  53. """An agent that executes tools."""
  54. def __init__(self, description: str, tools: List[Tool]) -> None:
  55. super().__init__(description)
  56. self._tools = tools
  57. @message_handler
  58. async def handle_tool_call(self, message: ToolExecutionTask, ctx: MessageContext) -> None:
  59. """Handle a tool execution task. This method executes the tool and publishes the result."""
  60. # Find the tool
  61. tool = next((tool for tool in self._tools if tool.name == message.function_call.name), None)
  62. if tool is None:
  63. result_as_str = f"Error: Tool not found: {message.function_call.name}"
  64. else:
  65. try:
  66. arguments = json.loads(message.function_call.arguments)
  67. result = await tool.run_json(args=arguments, cancellation_token=ctx.cancellation_token)
  68. result_as_str = tool.return_value_as_string(result)
  69. except json.JSONDecodeError:
  70. result_as_str = f"Error: Invalid arguments: {message.function_call.arguments}"
  71. except Exception as e:
  72. result_as_str = f"Error: {e}"
  73. task_result = ToolExecutionTaskResult(
  74. session_id=message.session_id,
  75. result=FunctionExecutionResult(content=result_as_str, call_id=message.function_call.id),
  76. )
  77. await self.publish_message(task_result, topic_id=DefaultTopicId())
  78. class ToolUseAgent(RoutedAgent):
  79. """An agent that uses tools to perform tasks. It doesn't execute the tools
  80. by itself, but delegates the execution to ToolExecutorAgent using pub/sub
  81. mechanism."""
  82. def __init__(
  83. self,
  84. description: str,
  85. system_messages: List[SystemMessage],
  86. model_client: ChatCompletionClient,
  87. tools: List[Tool],
  88. ) -> None:
  89. super().__init__(description)
  90. self._model_client = model_client
  91. self._system_messages = system_messages
  92. self._tools = tools
  93. self._sessions: Dict[str, List[LLMMessage]] = {}
  94. self._tool_results: Dict[str, List[ToolExecutionTaskResult]] = {}
  95. self._tool_counter: Dict[str, int] = {}
  96. @message_handler
  97. async def handle_user_message(self, message: UserRequest, ctx: MessageContext) -> None:
  98. """Handle a user message. This method calls the model. If the model response is a string,
  99. it publishes the response. If the model response is a list of function calls, it publishes
  100. the function calls to the tool executor agent."""
  101. session_id = str(uuid.uuid4())
  102. self._sessions.setdefault(session_id, []).append(UserMessage(content=message.content, source="User"))
  103. response = await self._model_client.create(
  104. self._system_messages + self._sessions[session_id], tools=self._tools
  105. )
  106. self._sessions[session_id].append(AssistantMessage(content=response.content, source=self.metadata["type"]))
  107. if isinstance(response.content, str):
  108. # If the response is a string, just publish the response.
  109. response_message = AgentResponse(content=response.content)
  110. await self.publish_message(response_message, topic_id=DefaultTopicId())
  111. print(f"AI Response: {response.content}")
  112. return
  113. # Handle the response as a list of function calls.
  114. assert isinstance(response.content, list) and all(isinstance(item, FunctionCall) for item in response.content)
  115. self._tool_results.setdefault(session_id, [])
  116. self._tool_counter.setdefault(session_id, 0)
  117. # Publish the function calls to the tool executor agent.
  118. for function_call in response.content:
  119. task = ToolExecutionTask(session_id=session_id, function_call=function_call)
  120. self._tool_counter[session_id] += 1
  121. await self.publish_message(task, topic_id=DefaultTopicId())
  122. @message_handler
  123. async def handle_tool_result(self, message: ToolExecutionTaskResult, ctx: MessageContext) -> None:
  124. """Handle a tool execution result. This method aggregates the tool results and
  125. calls the model again to get another response. If the response is a string, it
  126. publishes the response. If the response is a list of function calls, it publishes
  127. the function calls to the tool executor agent."""
  128. self._tool_results[message.session_id].append(message)
  129. self._tool_counter[message.session_id] -= 1
  130. if self._tool_counter[message.session_id] > 0:
  131. # Not all tools have finished execution.
  132. return
  133. # All tools have finished execution.
  134. # Aggregate tool results into a single LLM message.
  135. result = FunctionExecutionResultMessage(content=[r.result for r in self._tool_results[message.session_id]])
  136. # Clear the tool results.
  137. self._tool_results[message.session_id].clear()
  138. # Get another response from the model.
  139. self._sessions[message.session_id].append(result)
  140. response = await self._model_client.create(
  141. self._system_messages + self._sessions[message.session_id], tools=self._tools
  142. )
  143. self._sessions[message.session_id].append(
  144. AssistantMessage(content=response.content, source=self.metadata["type"])
  145. )
  146. # If the response is a string, just publish the response.
  147. if isinstance(response.content, str):
  148. response_message = AgentResponse(content=response.content)
  149. await self.publish_message(response_message, topic_id=DefaultTopicId())
  150. self._tool_results.pop(message.session_id)
  151. self._tool_counter.pop(message.session_id)
  152. print(f"AI Response: {response.content}")
  153. return
  154. # Handle the response as a list of function calls.
  155. assert isinstance(response.content, list) and all(isinstance(item, FunctionCall) for item in response.content)
  156. # Publish the function calls to the tool executor agent.
  157. for function_call in response.content:
  158. task = ToolExecutionTask(session_id=message.session_id, function_call=function_call)
  159. self._tool_counter[message.session_id] += 1
  160. await self.publish_message(task, topic_id=DefaultTopicId())
  161. async def main() -> None:
  162. runtime = SingleThreadedAgentRuntime()
  163. # Define the tools.
  164. tools: List[Tool] = [
  165. PythonCodeExecutionTool(
  166. LocalCommandLineCodeExecutor(),
  167. )
  168. ]
  169. # Register agents.
  170. await runtime.register("tool_executor", lambda: ToolExecutorAgent("Tool Executor", tools))
  171. await runtime.add_subscription(TypeSubscription("default", "tool_executor"))
  172. await runtime.register(
  173. "tool_use_agent",
  174. lambda: ToolUseAgent(
  175. description="Tool Use Agent",
  176. system_messages=[SystemMessage("You are a helpful AI Assistant. Use your tools to solve problems.")],
  177. model_client=get_chat_completion_client_from_envs(model="gpt-4o-mini"),
  178. tools=tools,
  179. ),
  180. )
  181. await runtime.add_subscription(TypeSubscription("default", "tool_use_agent"))
  182. runtime.start()
  183. # Publish a task.
  184. await runtime.publish_message(
  185. UserRequest("Run the following Python code: print('Hello, World!')"), topic_id=TopicId("default", "default")
  186. )
  187. await runtime.stop_when_idle()
  188. if __name__ == "__main__":
  189. import logging
  190. logging.basicConfig(level=logging.WARNING)
  191. logging.getLogger("agnext").setLevel(logging.DEBUG)
  192. asyncio.run(main())