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.3 kB

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