From d7ae2038fb6e5851f42a4e011307e9e7240a3ca0 Mon Sep 17 00:00:00 2001 From: Eric Zhu Date: Fri, 23 Aug 2024 13:22:31 -0700 Subject: [PATCH] Rename TypeRoutedAgent to RoutedAgent; log on unhandled message. (#400) * Rename TypeRoutedAgent to RoutedAgent; log on unhandled message. * format * Deprecation warning * add test for routed agent * add TypeRoutedAgent import * fix import --- .../docs/src/cookbook/langgraph-agent.ipynb | 6 +- .../docs/src/cookbook/llamaindex-agent.ipynb | 6 +- .../src/cookbook/openai-assistant-agent.ipynb | 6 +- .../cookbook/termination-with-intervention.md | 4 +- python/docs/src/cookbook/type-routed-agent.md | 6 +- .../message-and-communication.ipynb | 846 +++++++++--------- .../src/getting-started/model-clients.ipynb | 4 +- .../multi-agent-design-patterns.ipynb | 6 +- python/docs/src/getting-started/tools.ipynb | 6 +- python/samples/byoa/langgraph_agent.py | 4 +- python/samples/byoa/llamaindex_agent.py | 4 +- .../common/agents/_chat_completion_agent.py | 4 +- .../common/agents/_image_generation_agent.py | 4 +- .../samples/common/agents/_oai_assistant.py | 4 +- python/samples/common/agents/_user_proxy.py | 4 +- .../common/patterns/_group_chat_manager.py | 4 +- .../common/patterns/_orchestrator_chat.py | 4 +- python/samples/core/inner_outer_direct.py | 6 +- python/samples/core/one_agent_direct.py | 4 +- python/samples/core/two_agents_pub_sub.py | 4 +- python/samples/demos/assistant.py | 4 +- python/samples/demos/chat_room.py | 4 +- python/samples/demos/utils.py | 4 +- python/samples/marketing-agents/auditor.py | 4 +- .../marketing-agents/graphic_designer.py | 4 +- python/samples/marketing-agents/test_usage.py | 4 +- python/samples/patterns/coder_executor.py | 6 +- python/samples/patterns/coder_reviewer.py | 6 +- python/samples/patterns/group_chat.py | 6 +- python/samples/patterns/mixture_of_agents.py | 6 +- python/samples/patterns/multi_agent_debate.py | 6 +- python/samples/tool-use/coding_direct.py | 4 +- python/samples/tool-use/coding_pub_sub.py | 6 +- python/samples/worker/run_worker_pub_sub.py | 6 +- python/samples/worker/run_worker_rpc.py | 6 +- python/src/agnext/components/__init__.py | 3 +- ..._type_routed_agent.py => _routed_agent.py} | 17 +- .../components/tool_agent/_tool_agent.py | 4 +- .../src/team_one/agents/base_agent.py | 4 +- .../src/team_one/agents/reflex_agents.py | 4 +- python/tests/test_cancellation.py | 6 +- python/tests/test_routed_agent.py | 24 + python/tests/test_types.py | 2 +- python/tests/test_utils/__init__.py | 6 +- 44 files changed, 557 insertions(+), 525 deletions(-) rename python/src/agnext/components/{_type_routed_agent.py => _routed_agent.py} (92%) create mode 100644 python/tests/test_routed_agent.py diff --git a/python/docs/src/cookbook/langgraph-agent.ipynb b/python/docs/src/cookbook/langgraph-agent.ipynb index 9a605ff93a84..5debab0c06cf 100644 --- a/python/docs/src/cookbook/langgraph-agent.ipynb +++ b/python/docs/src/cookbook/langgraph-agent.ipynb @@ -44,7 +44,7 @@ "from typing import Any, Callable, List, Literal\n", "\n", "from agnext.application import SingleThreadedAgentRuntime\n", - "from agnext.components import TypeRoutedAgent, message_handler\n", + "from agnext.components import RoutedAgent, message_handler\n", "from agnext.core import AgentId, MessageContext\n", "from azure.identity import DefaultAzureCredential, get_bearer_token_provider\n", "from langchain_core.messages import HumanMessage, SystemMessage\n", @@ -107,7 +107,7 @@ "metadata": {}, "outputs": [], "source": [ - "class LangGraphToolUseAgent(TypeRoutedAgent):\n", + "class LangGraphToolUseAgent(RoutedAgent):\n", " def __init__(self, description: str, model: ChatOpenAI, tools: List[Callable[..., Any]]) -> None: # pyright: ignore\n", " super().__init__(description)\n", " self._model = model.bind_tools(tools) # pyright: ignore\n", @@ -297,4 +297,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/python/docs/src/cookbook/llamaindex-agent.ipynb b/python/docs/src/cookbook/llamaindex-agent.ipynb index 65a5b865028d..2941f5fc6f2b 100644 --- a/python/docs/src/cookbook/llamaindex-agent.ipynb +++ b/python/docs/src/cookbook/llamaindex-agent.ipynb @@ -43,7 +43,7 @@ "from typing import List, Optional\n", "\n", "from agnext.application import SingleThreadedAgentRuntime\n", - "from agnext.components import TypeRoutedAgent, message_handler\n", + "from agnext.components import RoutedAgent, message_handler\n", "from agnext.core import AgentId, MessageContext\n", "from azure.identity import DefaultAzureCredential, get_bearer_token_provider\n", "from llama_index.core import Settings\n", @@ -102,7 +102,7 @@ "metadata": {}, "outputs": [], "source": [ - "class LlamaIndexAgent(TypeRoutedAgent):\n", + "class LlamaIndexAgent(RoutedAgent):\n", " def __init__(self, description: str, llama_index_agent: AgentRunner, memory: BaseMemory | None = None) -> None:\n", " super().__init__(description)\n", "\n", @@ -530,4 +530,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/python/docs/src/cookbook/openai-assistant-agent.ipynb b/python/docs/src/cookbook/openai-assistant-agent.ipynb index 43e2b43c4200..9a87ad746f6a 100644 --- a/python/docs/src/cookbook/openai-assistant-agent.ipynb +++ b/python/docs/src/cookbook/openai-assistant-agent.ipynb @@ -104,13 +104,13 @@ "from typing import Any, Callable, List\n", "\n", "import aiofiles\n", - "from agnext.components import TypeRoutedAgent, message_handler\n", + "from agnext.components import RoutedAgent, message_handler\n", "from agnext.core import AgentId, MessageContext\n", "from openai import AsyncAssistantEventHandler, AsyncClient\n", "from openai.types.beta.thread import ToolResources, ToolResourcesFileSearch\n", "\n", "\n", - "class OpenAIAssistantAgent(TypeRoutedAgent):\n", + "class OpenAIAssistantAgent(RoutedAgent):\n", " \"\"\"An agent implementation that uses the OpenAI Assistant API to generate\n", " responses.\n", "\n", @@ -839,4 +839,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/python/docs/src/cookbook/termination-with-intervention.md b/python/docs/src/cookbook/termination-with-intervention.md index 4e4161439da1..10eb40066427 100644 --- a/python/docs/src/cookbook/termination-with-intervention.md +++ b/python/docs/src/cookbook/termination-with-intervention.md @@ -12,7 +12,7 @@ from dataclasses import dataclass from typing import Any from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.core import AgentId, CancellationToken from agnext.core.intervention import DefaultInterventionHandler ``` @@ -28,7 +28,7 @@ class Termination: We code our agent to publish a termination message when it decides it is time to terminate. ```python -class AnAgent(TypeRoutedAgent): +class AnAgent(RoutedAgent): def __init__(self) -> None: super().__init__("MyAgent") self.received = 0 diff --git a/python/docs/src/cookbook/type-routed-agent.md b/python/docs/src/cookbook/type-routed-agent.md index 77ef441605ae..e3dbc02685f9 100644 --- a/python/docs/src/cookbook/type-routed-agent.md +++ b/python/docs/src/cookbook/type-routed-agent.md @@ -1,6 +1,6 @@ # Using Type Routed Agent -To make it easier to implement agents that respond to certain message types there is a base class called {py:class}`~agnext.components.TypeRoutedAgent`. This class provides a simple decorator pattern for associating message types with message handlers. +To make it easier to implement agents that respond to certain message types there is a base class called {py:class}`~agnext.components.RoutedAgent`. This class provides a simple decorator pattern for associating message types with message handlers. The decorator {py:func}`agnext.components.message_handler` should be added to functions in the class that are intended to handle messages. These functions have a specific signature that needs to be followed for it to be recognized as a message handler. @@ -25,7 +25,7 @@ One important thing to point out is that when an agent is constructed it must be ```python from dataclasses import dataclass from typing import List, Union -from agnext.components import TypeRoutedAgent, message_handler, Image +from agnext.components import RoutedAgent, message_handler, Image from agnext.core import AgentRuntime, CancellationToken @dataclass @@ -43,7 +43,7 @@ class Reset: pass -class MyAgent(TypeRoutedAgent): +class MyAgent(RoutedAgent): def __init__(self): super().__init__(description="I am a demo agent") self._received_count = 0 diff --git a/python/docs/src/getting-started/message-and-communication.ipynb b/python/docs/src/getting-started/message-and-communication.ipynb index 50bf94a4abfb..3f12d7e91bd1 100644 --- a/python/docs/src/getting-started/message-and-communication.ipynb +++ b/python/docs/src/getting-started/message-and-communication.ipynb @@ -1,424 +1,424 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Message and Communication\n", - "\n", - "An agent in AGNext can react to, send, and publish messages,\n", - "and messages are the only means through which agents can communicate\n", - "with each other." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Messages\n", - "\n", - "Messages are serializable objects, they can be defined using:\n", - "\n", - "- A subclass of Pydantic's {py:class}`pydantic.BaseModel`, or\n", - "- A dataclass\n", - "\n", - "For example:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from dataclasses import dataclass\n", - "\n", - "\n", - "@dataclass\n", - "class TextMessage:\n", - " content: str\n", - " source: str\n", - "\n", - "\n", - "@dataclass\n", - "class ImageMessage:\n", - " url: str\n", - " source: str" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```{note}\n", - "Messages are purely data, and should not contain any logic.\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Message Handlers\n", - "\n", - "When an agent receives a message the runtime will invoke the agent's message handler\n", - "({py:meth}`~agnext.core.Agent.on_message`) which should implement the agents message handling logic.\n", - "If this message cannot be handled by the agent, the agent should raise a\n", - "{py:class}`~agnext.core.exceptions.CantHandleException`.\n", - "\n", - "For convenience, the {py:class}`~agnext.components.TypeRoutedAgent` base class\n", - "provides the {py:meth}`~agnext.components.message_handler` decorator\n", - "for associating message types with message handlers,\n", - "so developers do not need to implement the {py:meth}`~agnext.core.Agent.on_message` method.\n", - "\n", - "For example, the following type-routed agent responds to `TextMessage` and `ImageMessage`\n", - "using different message handlers:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "from agnext.application import SingleThreadedAgentRuntime\n", - "from agnext.components import TypeRoutedAgent, message_handler\n", - "from agnext.core import AgentId, MessageContext\n", - "\n", - "\n", - "class MyAgent(TypeRoutedAgent):\n", - " @message_handler\n", - " async def on_text_message(self, message: TextMessage, ctx: MessageContext) -> None:\n", - " print(f\"Hello, {message.source}, you said {message.content}!\")\n", - "\n", - " @message_handler\n", - " async def on_image_message(self, message: ImageMessage, ctx: MessageContext) -> None:\n", - " print(f\"Hello, {message.source}, you sent me {message.url}!\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create the agent runtime and register the agent (see [Agent and Agent Runtime](agent-and-agent-runtime.ipynb)):" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "runtime = SingleThreadedAgentRuntime()\n", - "await runtime.register(\"my_agent\", lambda: MyAgent(\"My Agent\"))\n", - "agent = AgentId(\"my_agent\", \"default\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Test this agent with `TextMessage` and `ImageMessage`." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Hello, User, you said Hello, World!!\n", - "Hello, User, you sent me https://example.com/image.jpg!\n" - ] - } - ], - "source": [ - "runtime.start()\n", - "await runtime.send_message(TextMessage(content=\"Hello, World!\", source=\"User\"), agent)\n", - "await runtime.send_message(ImageMessage(url=\"https://example.com/image.jpg\", source=\"User\"), agent)\n", - "await runtime.stop_when_idle()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Communication\n", - "\n", - "There are two types of communication in AGNext:\n", - "\n", - "- **Direct communication**: An agent sends a direct message to another agent.\n", - "- **Broadcast communication**: An agent publishes a message to all agents in the same namespace.\n", - "\n", - "### Direct Communication\n", - "\n", - "To send a direct message to another agent, within a message handler use\n", - "the {py:meth}`agnext.core.BaseAgent.send_message` method,\n", - "from the runtime use the {py:meth}`agnext.core.AgentRuntime.send_message` method.\n", - "Awaiting calls to these methods will return the return value of the\n", - "receiving agent's message handler.\n", - "\n", - "```{note}\n", - "If the invoked agent raises an exception while the sender is awaiting,\n", - "the exception will be propagated back to the sender.\n", - "```\n", - "\n", - "#### Request/Response\n", - "\n", - "Direct communication can be used for request/response scenarios,\n", - "where the sender expects a response from the receiver.\n", - "The receiver can respond to the message by returning a value from its message handler.\n", - "You can think of this as a function call between agents.\n", - "\n", - "For example, consider the following type-routed agent:" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "from dataclasses import dataclass\n", - "\n", - "from agnext.application import SingleThreadedAgentRuntime\n", - "from agnext.components import TypeRoutedAgent, message_handler\n", - "from agnext.core import MessageContext\n", - "\n", - "\n", - "@dataclass\n", - "class Message:\n", - " content: str\n", - "\n", - "\n", - "class InnerAgent(TypeRoutedAgent):\n", - " @message_handler\n", - " async def on_my_message(self, message: Message, ctx: MessageContext) -> Message:\n", - " return Message(content=f\"Hello from inner, {message.content}\")\n", - "\n", - "\n", - "class OuterAgent(TypeRoutedAgent):\n", - " def __init__(self, description: str, inner_agent_type: str):\n", - " super().__init__(description)\n", - " self.inner_agent_id = AgentId(inner_agent_type, self.id.key)\n", - "\n", - " @message_handler\n", - " async def on_my_message(self, message: Message, ctx: MessageContext) -> None:\n", - " print(f\"Received message: {message.content}\")\n", - " # Send a direct message to the inner agent and receves a response.\n", - " response = await self.send_message(Message(f\"Hello from outer, {message.content}\"), self.inner_agent_id)\n", - " print(f\"Received inner response: {response.content}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Upone receving a message, the `OuterAgent` sends a direct message to the `InnerAgent` and receives\n", - "a message in response.\n", - "\n", - "We can test these agents by sending a `Message` to the `OuterAgent`." - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Received message: Hello, World!\n", - "Received inner response: Hello from inner, Hello from outer, Hello, World!\n" - ] - } - ], - "source": [ - "runtime = SingleThreadedAgentRuntime()\n", - "await runtime.register(\"inner_agent\", lambda: InnerAgent(\"InnerAgent\"))\n", - "await runtime.register(\"outer_agent\", lambda: OuterAgent(\"OuterAgent\", \"InnerAgent\"))\n", - "runtime.start()\n", - "outer = AgentId(\"outer_agent\", \"default\")\n", - "await runtime.send_message(Message(content=\"Hello, World!\"), outer)\n", - "await runtime.stop_when_idle()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Both outputs are produced by the `OuterAgent`'s message handler, however the second output is based on the response from the `InnerAgent`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Broadcast Communication\n", - "\n", - "Broadcast communication is effectively the publish/subscribe model.\n", - "As part of the base agent ({py:class}`~agnext.core.BaseAgent`) implementation,\n", - "it must advertise the message types that\n", - "it would like to receive when published ({py:attr}`~agnext.core.AgentMetadata.subscriptions`).\n", - "If one of these messages is published, the agent's message handler will be invoked.\n", - "\n", - "The key difference between direct and broadcast communication is that broadcast\n", - "communication cannot be used for request/response scenarios.\n", - "When an agent publishes a message it is one way only, it cannot receive a response\n", - "from any other agent, even if a receiving agent sends a response.\n", - "\n", - "```{note}\n", - "An agent receiving a message does not know if it is handling a published or direct message.\n", - "So, if a response is given to a published message, it will be thrown away.\n", - "```\n", - "\n", - "To publish a message to all agents in the same namespace,\n", - "use the {py:meth}`agnext.core.BaseAgent.publish_message` method.\n", - "This call must still be awaited to allow the runtime to deliver the message to all agents,\n", - "but it will always return `None`.\n", - "If an agent raises an exception while handling a published message,\n", - "this will be logged but will not be propagated back to the publishing agent.\n", - "\n", - "The following example shows a `BroadcastingAgent` that publishes a message\n", - "upong receiving a message. A `ReceivingAgent` that prints the message\n", - "it receives." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [], - "source": [ - "from agnext.application import SingleThreadedAgentRuntime\n", - "from agnext.components import TypeRoutedAgent, message_handler\n", - "from agnext.core import MessageContext, TopicId\n", - "\n", - "\n", - "class BroadcastingAgent(TypeRoutedAgent):\n", - " @message_handler\n", - " async def on_my_message(self, message: Message, ctx: MessageContext) -> None:\n", - " # Publish a message to all agents in the same namespace.\n", - " assert ctx.topic_id is not None\n", - " await self.publish_message(\n", - " Message(f\"Publishing a message: {message.content}!\"), topic_id=TopicId(\"deafult\", self.id.key)\n", - " )\n", - "\n", - "\n", - "class ReceivingAgent(TypeRoutedAgent):\n", - " @message_handler\n", - " async def on_my_message(self, message: Message, ctx: MessageContext) -> None:\n", - " print(f\"Received a message: {message.content}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Sending a direct message to the `BroadcastingAgent` will result in a message being published by\n", - "the `BroadcastingAgent` and received by the `ReceivingAgent`." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Received a message: Publishing a message: Hello, World!!\n" - ] - } - ], - "source": [ - "from agnext.components import TypeSubscription\n", - "\n", - "runtime = SingleThreadedAgentRuntime()\n", - "await runtime.register(\"broadcasting_agent\", lambda: BroadcastingAgent(\"Broadcasting Agent\"))\n", - "await runtime.register(\"receiving_agent\", lambda: ReceivingAgent(\"Receiving Agent\"))\n", - "await runtime.add_subscription(TypeSubscription(\"default\", \"broadcasting_agent\"))\n", - "await runtime.add_subscription(TypeSubscription(\"default\", \"receiving_agent\"))\n", - "runtime.start()\n", - "await runtime.send_message(Message(\"Hello, World!\"), AgentId(\"broadcasting_agent\", \"default\"))\n", - "await runtime.stop()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To publish a message to all agents outside of an agent handling a message,\n", - "the message should be published via the runtime with the\n", - "{py:meth}`agnext.core.AgentRuntime.publish_message` method." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Received a message: Hello, World! From the runtime!\n", - "Received a message: Publishing a message: Hello, World! From the runtime!!\n" - ] - } - ], - "source": [ - "# Replace send_message with publish_message in the above example.\n", - "\n", - "runtime = SingleThreadedAgentRuntime()\n", - "await runtime.register(\"broadcasting_agent\", lambda: BroadcastingAgent(\"Broadcasting Agent\"))\n", - "await runtime.register(\"receiving_agent\", lambda: ReceivingAgent(\"Receiving Agent\"))\n", - "await runtime.add_subscription(TypeSubscription(\"default\", \"broadcasting_agent\"))\n", - "await runtime.add_subscription(TypeSubscription(\"default\", \"receiving_agent\"))\n", - "runtime.start()\n", - "await runtime.publish_message(Message(\"Hello, World! From the runtime!\"), topic_id=TopicId(\"default\", \"default\"))\n", - "await runtime.stop_when_idle()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The first output is from the `ReceivingAgent` that received a message published\n", - "by the runtime. The second output is from the `ReceivingAgent` that received\n", - "a message published by the `BroadcastingAgent`.\n", - "\n", - "```{note}\n", - "If an agent publishes a message type for which it is subscribed it will not\n", - "receive the message it published. This is to prevent infinite loops.\n", - "```" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "agnext", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Message and Communication\n", + "\n", + "An agent in AGNext can react to, send, and publish messages,\n", + "and messages are the only means through which agents can communicate\n", + "with each other." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Messages\n", + "\n", + "Messages are serializable objects, they can be defined using:\n", + "\n", + "- A subclass of Pydantic's {py:class}`pydantic.BaseModel`, or\n", + "- A dataclass\n", + "\n", + "For example:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "\n", + "\n", + "@dataclass\n", + "class TextMessage:\n", + " content: str\n", + " source: str\n", + "\n", + "\n", + "@dataclass\n", + "class ImageMessage:\n", + " url: str\n", + " source: str" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{note}\n", + "Messages are purely data, and should not contain any logic.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Message Handlers\n", + "\n", + "When an agent receives a message the runtime will invoke the agent's message handler\n", + "({py:meth}`~agnext.core.Agent.on_message`) which should implement the agents message handling logic.\n", + "If this message cannot be handled by the agent, the agent should raise a\n", + "{py:class}`~agnext.core.exceptions.CantHandleException`.\n", + "\n", + "For convenience, the {py:class}`~agnext.components.RoutedAgent` base class\n", + "provides the {py:meth}`~agnext.components.message_handler` decorator\n", + "for associating message types with message handlers,\n", + "so developers do not need to implement the {py:meth}`~agnext.core.Agent.on_message` method.\n", + "\n", + "For example, the following type-routed agent responds to `TextMessage` and `ImageMessage`\n", + "using different message handlers:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from agnext.application import SingleThreadedAgentRuntime\n", + "from agnext.components import RoutedAgent, message_handler\n", + "from agnext.core import AgentId, MessageContext\n", + "\n", + "\n", + "class MyAgent(RoutedAgent):\n", + " @message_handler\n", + " async def on_text_message(self, message: TextMessage, ctx: MessageContext) -> None:\n", + " print(f\"Hello, {message.source}, you said {message.content}!\")\n", + "\n", + " @message_handler\n", + " async def on_image_message(self, message: ImageMessage, ctx: MessageContext) -> None:\n", + " print(f\"Hello, {message.source}, you sent me {message.url}!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create the agent runtime and register the agent (see [Agent and Agent Runtime](agent-and-agent-runtime.ipynb)):" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "runtime = SingleThreadedAgentRuntime()\n", + "await runtime.register(\"my_agent\", lambda: MyAgent(\"My Agent\"))\n", + "agent = AgentId(\"my_agent\", \"default\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Test this agent with `TextMessage` and `ImageMessage`." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hello, User, you said Hello, World!!\n", + "Hello, User, you sent me https://example.com/image.jpg!\n" + ] + } + ], + "source": [ + "runtime.start()\n", + "await runtime.send_message(TextMessage(content=\"Hello, World!\", source=\"User\"), agent)\n", + "await runtime.send_message(ImageMessage(url=\"https://example.com/image.jpg\", source=\"User\"), agent)\n", + "await runtime.stop_when_idle()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Communication\n", + "\n", + "There are two types of communication in AGNext:\n", + "\n", + "- **Direct communication**: An agent sends a direct message to another agent.\n", + "- **Broadcast communication**: An agent publishes a message to all agents in the same namespace.\n", + "\n", + "### Direct Communication\n", + "\n", + "To send a direct message to another agent, within a message handler use\n", + "the {py:meth}`agnext.core.BaseAgent.send_message` method,\n", + "from the runtime use the {py:meth}`agnext.core.AgentRuntime.send_message` method.\n", + "Awaiting calls to these methods will return the return value of the\n", + "receiving agent's message handler.\n", + "\n", + "```{note}\n", + "If the invoked agent raises an exception while the sender is awaiting,\n", + "the exception will be propagated back to the sender.\n", + "```\n", + "\n", + "#### Request/Response\n", + "\n", + "Direct communication can be used for request/response scenarios,\n", + "where the sender expects a response from the receiver.\n", + "The receiver can respond to the message by returning a value from its message handler.\n", + "You can think of this as a function call between agents.\n", + "\n", + "For example, consider the following type-routed agent:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "\n", + "from agnext.application import SingleThreadedAgentRuntime\n", + "from agnext.components import RoutedAgent, message_handler\n", + "from agnext.core import MessageContext\n", + "\n", + "\n", + "@dataclass\n", + "class Message:\n", + " content: str\n", + "\n", + "\n", + "class InnerAgent(RoutedAgent):\n", + " @message_handler\n", + " async def on_my_message(self, message: Message, ctx: MessageContext) -> Message:\n", + " return Message(content=f\"Hello from inner, {message.content}\")\n", + "\n", + "\n", + "class OuterAgent(RoutedAgent):\n", + " def __init__(self, description: str, inner_agent_type: str):\n", + " super().__init__(description)\n", + " self.inner_agent_id = AgentId(inner_agent_type, self.id.key)\n", + "\n", + " @message_handler\n", + " async def on_my_message(self, message: Message, ctx: MessageContext) -> None:\n", + " print(f\"Received message: {message.content}\")\n", + " # Send a direct message to the inner agent and receves a response.\n", + " response = await self.send_message(Message(f\"Hello from outer, {message.content}\"), self.inner_agent_id)\n", + " print(f\"Received inner response: {response.content}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Upone receving a message, the `OuterAgent` sends a direct message to the `InnerAgent` and receives\n", + "a message in response.\n", + "\n", + "We can test these agents by sending a `Message` to the `OuterAgent`." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Received message: Hello, World!\n", + "Received inner response: Hello from inner, Hello from outer, Hello, World!\n" + ] + } + ], + "source": [ + "runtime = SingleThreadedAgentRuntime()\n", + "await runtime.register(\"inner_agent\", lambda: InnerAgent(\"InnerAgent\"))\n", + "await runtime.register(\"outer_agent\", lambda: OuterAgent(\"OuterAgent\", \"InnerAgent\"))\n", + "runtime.start()\n", + "outer = AgentId(\"outer_agent\", \"default\")\n", + "await runtime.send_message(Message(content=\"Hello, World!\"), outer)\n", + "await runtime.stop_when_idle()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Both outputs are produced by the `OuterAgent`'s message handler, however the second output is based on the response from the `InnerAgent`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Broadcast Communication\n", + "\n", + "Broadcast communication is effectively the publish/subscribe model.\n", + "As part of the base agent ({py:class}`~agnext.core.BaseAgent`) implementation,\n", + "it must advertise the message types that\n", + "it would like to receive when published ({py:attr}`~agnext.core.AgentMetadata.subscriptions`).\n", + "If one of these messages is published, the agent's message handler will be invoked.\n", + "\n", + "The key difference between direct and broadcast communication is that broadcast\n", + "communication cannot be used for request/response scenarios.\n", + "When an agent publishes a message it is one way only, it cannot receive a response\n", + "from any other agent, even if a receiving agent sends a response.\n", + "\n", + "```{note}\n", + "An agent receiving a message does not know if it is handling a published or direct message.\n", + "So, if a response is given to a published message, it will be thrown away.\n", + "```\n", + "\n", + "To publish a message to all agents in the same namespace,\n", + "use the {py:meth}`agnext.core.BaseAgent.publish_message` method.\n", + "This call must still be awaited to allow the runtime to deliver the message to all agents,\n", + "but it will always return `None`.\n", + "If an agent raises an exception while handling a published message,\n", + "this will be logged but will not be propagated back to the publishing agent.\n", + "\n", + "The following example shows a `BroadcastingAgent` that publishes a message\n", + "upong receiving a message. A `ReceivingAgent` that prints the message\n", + "it receives." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "from agnext.application import SingleThreadedAgentRuntime\n", + "from agnext.components import RoutedAgent, message_handler\n", + "from agnext.core import MessageContext, TopicId\n", + "\n", + "\n", + "class BroadcastingAgent(RoutedAgent):\n", + " @message_handler\n", + " async def on_my_message(self, message: Message, ctx: MessageContext) -> None:\n", + " # Publish a message to all agents in the same namespace.\n", + " assert ctx.topic_id is not None\n", + " await self.publish_message(\n", + " Message(f\"Publishing a message: {message.content}!\"), topic_id=TopicId(\"deafult\", self.id.key)\n", + " )\n", + "\n", + "\n", + "class ReceivingAgent(RoutedAgent):\n", + " @message_handler\n", + " async def on_my_message(self, message: Message, ctx: MessageContext) -> None:\n", + " print(f\"Received a message: {message.content}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Sending a direct message to the `BroadcastingAgent` will result in a message being published by\n", + "the `BroadcastingAgent` and received by the `ReceivingAgent`." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Received a message: Publishing a message: Hello, World!!\n" + ] + } + ], + "source": [ + "from agnext.components import TypeSubscription\n", + "\n", + "runtime = SingleThreadedAgentRuntime()\n", + "await runtime.register(\"broadcasting_agent\", lambda: BroadcastingAgent(\"Broadcasting Agent\"))\n", + "await runtime.register(\"receiving_agent\", lambda: ReceivingAgent(\"Receiving Agent\"))\n", + "await runtime.add_subscription(TypeSubscription(\"default\", \"broadcasting_agent\"))\n", + "await runtime.add_subscription(TypeSubscription(\"default\", \"receiving_agent\"))\n", + "runtime.start()\n", + "await runtime.send_message(Message(\"Hello, World!\"), AgentId(\"broadcasting_agent\", \"default\"))\n", + "await runtime.stop()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To publish a message to all agents outside of an agent handling a message,\n", + "the message should be published via the runtime with the\n", + "{py:meth}`agnext.core.AgentRuntime.publish_message` method." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Received a message: Hello, World! From the runtime!\n", + "Received a message: Publishing a message: Hello, World! From the runtime!!\n" + ] + } + ], + "source": [ + "# Replace send_message with publish_message in the above example.\n", + "\n", + "runtime = SingleThreadedAgentRuntime()\n", + "await runtime.register(\"broadcasting_agent\", lambda: BroadcastingAgent(\"Broadcasting Agent\"))\n", + "await runtime.register(\"receiving_agent\", lambda: ReceivingAgent(\"Receiving Agent\"))\n", + "await runtime.add_subscription(TypeSubscription(\"default\", \"broadcasting_agent\"))\n", + "await runtime.add_subscription(TypeSubscription(\"default\", \"receiving_agent\"))\n", + "runtime.start()\n", + "await runtime.publish_message(Message(\"Hello, World! From the runtime!\"), topic_id=TopicId(\"default\", \"default\"))\n", + "await runtime.stop_when_idle()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The first output is from the `ReceivingAgent` that received a message published\n", + "by the runtime. The second output is from the `ReceivingAgent` that received\n", + "a message published by the `BroadcastingAgent`.\n", + "\n", + "```{note}\n", + "If an agent publishes a message type for which it is subscribed it will not\n", + "receive the message it published. This is to prevent infinite loops.\n", + "```" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "agnext", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/python/docs/src/getting-started/model-clients.ipynb b/python/docs/src/getting-started/model-clients.ipynb index d45ef528eeea..cb1d0349ed67 100644 --- a/python/docs/src/getting-started/model-clients.ipynb +++ b/python/docs/src/getting-started/model-clients.ipynb @@ -227,7 +227,7 @@ "from dataclasses import dataclass\n", "\n", "from agnext.application import SingleThreadedAgentRuntime\n", - "from agnext.components import TypeRoutedAgent, message_handler\n", + "from agnext.components import RoutedAgent, message_handler\n", "from agnext.components.models import ChatCompletionClient, OpenAIChatCompletionClient, SystemMessage, UserMessage\n", "from agnext.core import MessageContext\n", "\n", @@ -237,7 +237,7 @@ " content: str\n", "\n", "\n", - "class SimpleAgent(TypeRoutedAgent):\n", + "class SimpleAgent(RoutedAgent):\n", " def __init__(self, model_client: ChatCompletionClient) -> None:\n", " super().__init__(\"A simple agent\")\n", " self._system_messages = [SystemMessage(\"You are a helpful AI assistant.\")]\n", diff --git a/python/docs/src/getting-started/multi-agent-design-patterns.ipynb b/python/docs/src/getting-started/multi-agent-design-patterns.ipynb index b62174b94120..14799b12e674 100644 --- a/python/docs/src/getting-started/multi-agent-design-patterns.ipynb +++ b/python/docs/src/getting-started/multi-agent-design-patterns.ipynb @@ -123,7 +123,7 @@ "import uuid\n", "from typing import Dict, List, Union\n", "\n", - "from agnext.components import TypeRoutedAgent, message_handler\n", + "from agnext.components import RoutedAgent, message_handler\n", "from agnext.components.models import (\n", " AssistantMessage,\n", " ChatCompletionClient,\n", @@ -150,7 +150,7 @@ "metadata": {}, "outputs": [], "source": [ - "class CoderAgent(TypeRoutedAgent):\n", + "class CoderAgent(RoutedAgent):\n", " \"\"\"An agent that performs code writing tasks.\"\"\"\n", "\n", " def __init__(self, model_client: ChatCompletionClient) -> None:\n", @@ -293,7 +293,7 @@ "metadata": {}, "outputs": [], "source": [ - "class ReviewerAgent(TypeRoutedAgent):\n", + "class ReviewerAgent(RoutedAgent):\n", " \"\"\"An agent that performs code review tasks.\"\"\"\n", "\n", " def __init__(self, model_client: ChatCompletionClient) -> None:\n", diff --git a/python/docs/src/getting-started/tools.ipynb b/python/docs/src/getting-started/tools.ipynb index 8a413297e7ee..4aa35c23fcbb 100644 --- a/python/docs/src/getting-started/tools.ipynb +++ b/python/docs/src/getting-started/tools.ipynb @@ -135,7 +135,7 @@ "from typing import List\n", "\n", "from agnext.application import SingleThreadedAgentRuntime\n", - "from agnext.components import FunctionCall, TypeRoutedAgent, message_handler\n", + "from agnext.components import FunctionCall, RoutedAgent, message_handler\n", "from agnext.components.models import (\n", " AssistantMessage,\n", " ChatCompletionClient,\n", @@ -156,7 +156,7 @@ " content: str\n", "\n", "\n", - "class ToolUseAgent(TypeRoutedAgent):\n", + "class ToolUseAgent(RoutedAgent):\n", " def __init__(self, model_client: ChatCompletionClient, tool_schema: List[ToolSchema], tool_agent: AgentId) -> None:\n", " super().__init__(\"An agent with tools\")\n", " self._system_messages: List[LLMMessage] = [SystemMessage(\"You are a helpful AI assistant.\")]\n", @@ -321,4 +321,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/python/samples/byoa/langgraph_agent.py b/python/samples/byoa/langgraph_agent.py index 671464d6a333..55be01218f03 100644 --- a/python/samples/byoa/langgraph_agent.py +++ b/python/samples/byoa/langgraph_agent.py @@ -9,7 +9,7 @@ from typing import Any, Callable, List, Literal from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.core import AgentId, MessageContext from langchain_core.messages import HumanMessage, SystemMessage from langchain_core.tools import tool # pyright: ignore @@ -34,7 +34,7 @@ def get_weather(location: str) -> str: # Define the tool-use agent using LangGraph. -class LangGraphToolUseAgent(TypeRoutedAgent): +class LangGraphToolUseAgent(RoutedAgent): def __init__(self, description: str, model: ChatOpenAI, tools: List[Callable[..., Any]]) -> None: # pyright: ignore super().__init__(description) self._model = model.bind_tools(tools) # pyright: ignore diff --git a/python/samples/byoa/llamaindex_agent.py b/python/samples/byoa/llamaindex_agent.py index ef7895261044..ab52e847554f 100644 --- a/python/samples/byoa/llamaindex_agent.py +++ b/python/samples/byoa/llamaindex_agent.py @@ -8,7 +8,7 @@ from typing import List, Optional from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.core import AgentId, MessageContext from llama_index.core import Settings from llama_index.core.agent import ReActAgent @@ -38,7 +38,7 @@ class Message: sources: Optional[List[Resource]] = None -class LlamaIndexAgent(TypeRoutedAgent): +class LlamaIndexAgent(RoutedAgent): def __init__(self, description: str, llama_index_agent: AgentRunner, memory: BaseMemory | None = None) -> None: super().__init__(description) diff --git a/python/samples/common/agents/_chat_completion_agent.py b/python/samples/common/agents/_chat_completion_agent.py index 3109e803051e..213c90bfcb23 100644 --- a/python/samples/common/agents/_chat_completion_agent.py +++ b/python/samples/common/agents/_chat_completion_agent.py @@ -4,7 +4,7 @@ from agnext.components import ( FunctionCall, - TypeRoutedAgent, + RoutedAgent, message_handler, ) from agnext.components.memory import ChatMemory @@ -32,7 +32,7 @@ from ..utils import convert_messages_to_llm_messages -class ChatCompletionAgent(TypeRoutedAgent): +class ChatCompletionAgent(RoutedAgent): """An agent implementation that uses the ChatCompletion API to gnenerate responses and execute tools. diff --git a/python/samples/common/agents/_image_generation_agent.py b/python/samples/common/agents/_image_generation_agent.py index 1aad9d9e4069..8789917622cd 100644 --- a/python/samples/common/agents/_image_generation_agent.py +++ b/python/samples/common/agents/_image_generation_agent.py @@ -3,7 +3,7 @@ import openai from agnext.components import ( Image, - TypeRoutedAgent, + RoutedAgent, message_handler, ) from agnext.components.memory import ChatMemory @@ -18,7 +18,7 @@ ) -class ImageGenerationAgent(TypeRoutedAgent): +class ImageGenerationAgent(RoutedAgent): """An agent that generates images using DALL-E models. It publishes the generated images as MultiModalMessage. diff --git a/python/samples/common/agents/_oai_assistant.py b/python/samples/common/agents/_oai_assistant.py index 954c957f852b..6539d578a328 100644 --- a/python/samples/common/agents/_oai_assistant.py +++ b/python/samples/common/agents/_oai_assistant.py @@ -1,7 +1,7 @@ from typing import Any, Callable, List, Mapping import openai -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.core import ( CancellationToken, MessageContext, # type: ignore @@ -12,7 +12,7 @@ from ..types import PublishNow, Reset, RespondNow, ResponseFormat, TextMessage -class OpenAIAssistantAgent(TypeRoutedAgent): +class OpenAIAssistantAgent(RoutedAgent): """An agent implementation that uses the OpenAI Assistant API to generate responses. diff --git a/python/samples/common/agents/_user_proxy.py b/python/samples/common/agents/_user_proxy.py index c04c59378203..69d1a491ecde 100644 --- a/python/samples/common/agents/_user_proxy.py +++ b/python/samples/common/agents/_user_proxy.py @@ -1,12 +1,12 @@ import asyncio -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.core import MessageContext from ..types import PublishNow, TextMessage -class UserProxyAgent(TypeRoutedAgent): +class UserProxyAgent(RoutedAgent): """An agent that proxies user input from the console. Override the `get_user_input` method to customize how user input is retrieved. diff --git a/python/samples/common/patterns/_group_chat_manager.py b/python/samples/common/patterns/_group_chat_manager.py index ad0760ea7a7a..cb8581d5dfbc 100644 --- a/python/samples/common/patterns/_group_chat_manager.py +++ b/python/samples/common/patterns/_group_chat_manager.py @@ -1,7 +1,7 @@ import logging from typing import Any, Callable, List, Mapping -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.components.memory import ChatMemory from agnext.components.models import ChatCompletionClient from agnext.core import AgentId, AgentProxy, MessageContext @@ -18,7 +18,7 @@ logger = logging.getLogger("agnext.events") -class GroupChatManager(TypeRoutedAgent): +class GroupChatManager(RoutedAgent): """An agent that manages a group chat through event-driven orchestration. Args: diff --git a/python/samples/common/patterns/_orchestrator_chat.py b/python/samples/common/patterns/_orchestrator_chat.py index accad556d4e8..974b20384f5b 100644 --- a/python/samples/common/patterns/_orchestrator_chat.py +++ b/python/samples/common/patterns/_orchestrator_chat.py @@ -1,7 +1,7 @@ import json from typing import Any, Sequence, Tuple -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.core import AgentId, AgentRuntime, MessageContext from ..types import Reset, RespondNow, ResponseFormat, TextMessage @@ -9,7 +9,7 @@ __all__ = ["OrchestratorChat"] -class OrchestratorChat(TypeRoutedAgent): +class OrchestratorChat(RoutedAgent): def __init__( self, description: str, diff --git a/python/samples/core/inner_outer_direct.py b/python/samples/core/inner_outer_direct.py index ab99cac97db6..e926c829f29b 100644 --- a/python/samples/core/inner_outer_direct.py +++ b/python/samples/core/inner_outer_direct.py @@ -11,7 +11,7 @@ from dataclasses import dataclass from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.core import AgentId, AgentInstantiationContext, MessageContext @@ -21,7 +21,7 @@ class MessageType: sender: str -class Inner(TypeRoutedAgent): +class Inner(RoutedAgent): def __init__(self) -> None: super().__init__("The inner agent") @@ -30,7 +30,7 @@ async def on_new_message(self, message: MessageType, ctx: MessageContext) -> Mes return MessageType(body=f"Inner: {message.body}", sender=self.metadata["type"]) -class Outer(TypeRoutedAgent): +class Outer(RoutedAgent): def __init__(self, inner: AgentId) -> None: super().__init__("The outer agent") self._inner = inner diff --git a/python/samples/core/one_agent_direct.py b/python/samples/core/one_agent_direct.py index d1203afd2126..6055c64e0b4b 100644 --- a/python/samples/core/one_agent_direct.py +++ b/python/samples/core/one_agent_direct.py @@ -11,7 +11,7 @@ from dataclasses import dataclass from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.components.models import ( ChatCompletionClient, SystemMessage, @@ -30,7 +30,7 @@ class Message: content: str -class ChatCompletionAgent(TypeRoutedAgent): +class ChatCompletionAgent(RoutedAgent): def __init__(self, description: str, model_client: ChatCompletionClient) -> None: super().__init__(description) self._system_messages = [SystemMessage("You are a helpful AI assistant.")] diff --git a/python/samples/core/two_agents_pub_sub.py b/python/samples/core/two_agents_pub_sub.py index f84de6d7ea08..fd18f23abd2f 100644 --- a/python/samples/core/two_agents_pub_sub.py +++ b/python/samples/core/two_agents_pub_sub.py @@ -17,7 +17,7 @@ from typing import List from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.components._type_subscription import TypeSubscription from agnext.components.models import ( AssistantMessage, @@ -40,7 +40,7 @@ class Message: content: str -class ChatCompletionAgent(TypeRoutedAgent): +class ChatCompletionAgent(RoutedAgent): """An agent that uses a chat completion model to respond to messages. It keeps a memory of the conversation and uses it to generate responses. It publishes a termination message when the termination word is mentioned.""" diff --git a/python/samples/demos/assistant.py b/python/samples/demos/assistant.py index c07eb191765b..2f9ce36540e5 100644 --- a/python/samples/demos/assistant.py +++ b/python/samples/demos/assistant.py @@ -12,7 +12,7 @@ import aiofiles import openai from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.core import AgentId, AgentRuntime, MessageContext from openai import AsyncAssistantEventHandler from openai.types.beta.thread import ToolResources @@ -31,7 +31,7 @@ sep = "-" * 50 -class UserProxyAgent(TypeRoutedAgent): +class UserProxyAgent(RoutedAgent): def __init__( # type: ignore self, client: openai.AsyncClient, # type: ignore diff --git a/python/samples/demos/chat_room.py b/python/samples/demos/chat_room.py index ccf068b2e225..842c8c6affdd 100644 --- a/python/samples/demos/chat_room.py +++ b/python/samples/demos/chat_room.py @@ -6,7 +6,7 @@ import sys from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.components.memory import ChatMemory from agnext.components.models import ChatCompletionClient, SystemMessage from agnext.core import AgentId, AgentInstantiationContext, AgentProxy, AgentRuntime @@ -22,7 +22,7 @@ # Define a custom agent that can handle chat room messages. -class ChatRoomAgent(TypeRoutedAgent): +class ChatRoomAgent(RoutedAgent): def __init__( self, name: str, diff --git a/python/samples/demos/utils.py b/python/samples/demos/utils.py index baf303c294cf..96e21d761fea 100644 --- a/python/samples/demos/utils.py +++ b/python/samples/demos/utils.py @@ -4,7 +4,7 @@ import sys from asyncio import Future -from agnext.components import Image, TypeRoutedAgent, message_handler +from agnext.components import Image, RoutedAgent, message_handler from agnext.core import AgentRuntime, CancellationToken from textual.app import App, ComposeResult from textual.containers import ScrollableContainer @@ -157,7 +157,7 @@ async def handle_tool_approval_request(self, message: ToolApprovalRequest) -> To return await future -class TextualUserAgent(TypeRoutedAgent): # type: ignore +class TextualUserAgent(RoutedAgent): # type: ignore """An agent that is used to receive messages from the runtime.""" def __init__(self, description: str, app: TextualChatApp) -> None: # type: ignore diff --git a/python/samples/marketing-agents/auditor.py b/python/samples/marketing-agents/auditor.py index 5d2366f05c9b..0d4e197dc26d 100644 --- a/python/samples/marketing-agents/auditor.py +++ b/python/samples/marketing-agents/auditor.py @@ -1,4 +1,4 @@ -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.components.models import ChatCompletionClient from agnext.components.models._types import SystemMessage from agnext.core import MessageContext @@ -15,7 +15,7 @@ """ -class AuditAgent(TypeRoutedAgent): +class AuditAgent(RoutedAgent): def __init__( self, model_client: ChatCompletionClient, diff --git a/python/samples/marketing-agents/graphic_designer.py b/python/samples/marketing-agents/graphic_designer.py index b7c3b879a294..75c4641ae8ce 100644 --- a/python/samples/marketing-agents/graphic_designer.py +++ b/python/samples/marketing-agents/graphic_designer.py @@ -3,14 +3,14 @@ import openai from agnext.components import ( - TypeRoutedAgent, + RoutedAgent, message_handler, ) from agnext.core import MessageContext from messages import ArticleCreated, GraphicDesignCreated -class GraphicDesignerAgent(TypeRoutedAgent): +class GraphicDesignerAgent(RoutedAgent): def __init__( self, client: openai.AsyncClient, diff --git a/python/samples/marketing-agents/test_usage.py b/python/samples/marketing-agents/test_usage.py index 8288aa89af17..55d61689cad4 100644 --- a/python/samples/marketing-agents/test_usage.py +++ b/python/samples/marketing-agents/test_usage.py @@ -2,14 +2,14 @@ import os from agnext.application import SingleThreadedAgentRuntime -from agnext.components import Image, TypeRoutedAgent, message_handler +from agnext.components import Image, RoutedAgent, message_handler from agnext.core import MessageContext, TopicId from app import build_app from dotenv import load_dotenv from messages import ArticleCreated, AuditorAlert, AuditText, GraphicDesignCreated -class Printer(TypeRoutedAgent): +class Printer(RoutedAgent): def __init__( self, ) -> None: diff --git a/python/samples/patterns/coder_executor.py b/python/samples/patterns/coder_executor.py index 6e7dc2aec60c..45529600aa6d 100644 --- a/python/samples/patterns/coder_executor.py +++ b/python/samples/patterns/coder_executor.py @@ -21,7 +21,7 @@ from typing import Dict, List from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.components._type_subscription import TypeSubscription from agnext.components.code_executor import CodeBlock, CodeExecutor, LocalCommandLineCodeExecutor from agnext.components.models import ( @@ -62,7 +62,7 @@ class CodeExecutionTaskResult: exit_code: int -class Coder(TypeRoutedAgent): +class Coder(RoutedAgent): """An agent that writes code.""" def __init__( @@ -144,7 +144,7 @@ async def handle_code_execution_result(self, message: CodeExecutionTaskResult, c ) -class Executor(TypeRoutedAgent): +class Executor(RoutedAgent): """An agent that executes code.""" def __init__(self, executor: CodeExecutor) -> None: diff --git a/python/samples/patterns/coder_reviewer.py b/python/samples/patterns/coder_reviewer.py index 77d763df76cf..80cf76d44071 100644 --- a/python/samples/patterns/coder_reviewer.py +++ b/python/samples/patterns/coder_reviewer.py @@ -21,7 +21,7 @@ from typing import Dict, List, Union from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.components._type_subscription import TypeSubscription from agnext.components.models import ( AssistantMessage, @@ -65,7 +65,7 @@ class CodeReviewResult: approved: bool -class ReviewerAgent(TypeRoutedAgent): +class ReviewerAgent(RoutedAgent): """An agent that performs code review tasks.""" def __init__( @@ -123,7 +123,7 @@ async def handle_code_review_task(self, message: CodeReviewTask, ctx: MessageCon ) -class CoderAgent(TypeRoutedAgent): +class CoderAgent(RoutedAgent): """An agent that performs code writing tasks.""" def __init__( diff --git a/python/samples/patterns/group_chat.py b/python/samples/patterns/group_chat.py index 9e8e902cc5e6..5d48d5ad75f7 100644 --- a/python/samples/patterns/group_chat.py +++ b/python/samples/patterns/group_chat.py @@ -18,7 +18,7 @@ from typing import List from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.components.models import ( AssistantMessage, ChatCompletionClient, @@ -50,7 +50,7 @@ class Termination: pass -class RoundRobinGroupChatManager(TypeRoutedAgent): +class RoundRobinGroupChatManager(RoutedAgent): def __init__( self, description: str, @@ -76,7 +76,7 @@ async def handle_message(self, message: Message, ctx: MessageContext) -> None: await self.send_message(RequestToSpeak(), speaker) -class GroupChatParticipant(TypeRoutedAgent): +class GroupChatParticipant(RoutedAgent): def __init__( self, description: str, diff --git a/python/samples/patterns/mixture_of_agents.py b/python/samples/patterns/mixture_of_agents.py index 28bc8372d8d9..c7d0159e6509 100644 --- a/python/samples/patterns/mixture_of_agents.py +++ b/python/samples/patterns/mixture_of_agents.py @@ -15,7 +15,7 @@ from typing import Dict, List from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.components._type_subscription import TypeSubscription from agnext.components.models import ChatCompletionClient, SystemMessage, UserMessage from agnext.core import MessageContext @@ -48,7 +48,7 @@ class AggregatorTaskResult: result: str -class ReferenceAgent(TypeRoutedAgent): +class ReferenceAgent(RoutedAgent): """The reference agent that handles each task independently.""" def __init__( @@ -72,7 +72,7 @@ async def handle_task(self, message: ReferenceAgentTask, ctx: MessageContext) -> await self.publish_message(task_result, topic_id=ctx.topic_id) -class AggregatorAgent(TypeRoutedAgent): +class AggregatorAgent(RoutedAgent): """The aggregator agent that distribute tasks to reference agents and aggregates the results.""" def __init__( diff --git a/python/samples/patterns/multi_agent_debate.py b/python/samples/patterns/multi_agent_debate.py index 188859068864..f819465f39e0 100644 --- a/python/samples/patterns/multi_agent_debate.py +++ b/python/samples/patterns/multi_agent_debate.py @@ -40,7 +40,7 @@ from typing import Dict, List, Tuple from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.components._type_subscription import TypeSubscription from agnext.components.models import ( AssistantMessage, @@ -92,7 +92,7 @@ class FinalSolverResponse: answer: str -class MathSolver(TypeRoutedAgent): +class MathSolver(RoutedAgent): def __init__(self, model_client: ChatCompletionClient, neighbor_names: List[str], max_round: int) -> None: super().__init__("A debator.") self._model_client = model_client @@ -185,7 +185,7 @@ async def handle_request(self, message: SolverRequest, ctx: MessageContext) -> N ) -class MathAggregator(TypeRoutedAgent): +class MathAggregator(RoutedAgent): def __init__(self, num_solvers: int) -> None: super().__init__("Math Aggregator") self._num_solvers = num_solvers diff --git a/python/samples/tool-use/coding_direct.py b/python/samples/tool-use/coding_direct.py index c837ebf83a1f..8aa73b86268f 100644 --- a/python/samples/tool-use/coding_direct.py +++ b/python/samples/tool-use/coding_direct.py @@ -17,7 +17,7 @@ from typing import List from agnext.application import SingleThreadedAgentRuntime -from agnext.components import FunctionCall, TypeRoutedAgent, message_handler +from agnext.components import FunctionCall, RoutedAgent, message_handler from agnext.components.code_executor import LocalCommandLineCodeExecutor from agnext.components.models import ( AssistantMessage, @@ -43,7 +43,7 @@ class Message: content: str -class ToolUseAgent(TypeRoutedAgent): +class ToolUseAgent(RoutedAgent): """An agent that uses tools to perform tasks. It executes the tools by itself by sending the tool execution task to itself.""" diff --git a/python/samples/tool-use/coding_pub_sub.py b/python/samples/tool-use/coding_pub_sub.py index 61970b9b2857..81d462af07cc 100644 --- a/python/samples/tool-use/coding_pub_sub.py +++ b/python/samples/tool-use/coding_pub_sub.py @@ -20,7 +20,7 @@ from typing import Dict, List from agnext.application import SingleThreadedAgentRuntime -from agnext.components import FunctionCall, TypeRoutedAgent, message_handler +from agnext.components import FunctionCall, RoutedAgent, message_handler from agnext.components._type_subscription import TypeSubscription from agnext.components.code_executor import LocalCommandLineCodeExecutor from agnext.components.models import ( @@ -63,7 +63,7 @@ class AgentResponse: content: str -class ToolExecutorAgent(TypeRoutedAgent): +class ToolExecutorAgent(RoutedAgent): """An agent that executes tools.""" def __init__(self, description: str, tools: List[Tool]) -> None: @@ -94,7 +94,7 @@ async def handle_tool_call(self, message: ToolExecutionTask, ctx: MessageContext await self.publish_message(task_result, topic_id=ctx.topic_id) -class ToolUseAgent(TypeRoutedAgent): +class ToolUseAgent(RoutedAgent): """An agent that uses tools to perform tasks. It doesn't execute the tools by itself, but delegates the execution to ToolExecutorAgent using pub/sub mechanism.""" diff --git a/python/samples/worker/run_worker_pub_sub.py b/python/samples/worker/run_worker_pub_sub.py index ea76f306a3b6..a2c8f7ae3d14 100644 --- a/python/samples/worker/run_worker_pub_sub.py +++ b/python/samples/worker/run_worker_pub_sub.py @@ -4,7 +4,7 @@ from typing import Any, NoReturn from agnext.application import WorkerAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.components._type_subscription import TypeSubscription from agnext.core import MESSAGE_TYPE_REGISTRY, MessageContext, TopicId @@ -34,7 +34,7 @@ class ReturnedFeedback: content: str -class ReceiveAgent(TypeRoutedAgent): +class ReceiveAgent(RoutedAgent): def __init__(self) -> None: super().__init__("Receive Agent") @@ -52,7 +52,7 @@ async def on_unhandled_message(self, message: Any, ctx: MessageContext) -> NoRet print(f"Unhandled message: {message}") -class GreeterAgent(TypeRoutedAgent): +class GreeterAgent(RoutedAgent): def __init__(self) -> None: super().__init__("Greeter Agent") diff --git a/python/samples/worker/run_worker_rpc.py b/python/samples/worker/run_worker_rpc.py index d7f3c9d31eb0..8980c1ea95ad 100644 --- a/python/samples/worker/run_worker_rpc.py +++ b/python/samples/worker/run_worker_rpc.py @@ -4,7 +4,7 @@ from typing import Any, NoReturn from agnext.application import WorkerAgentRuntime -from agnext.components import TypeRoutedAgent, TypeSubscription, message_handler +from agnext.components import RoutedAgent, TypeSubscription, message_handler from agnext.core import MESSAGE_TYPE_REGISTRY, AgentId, AgentInstantiationContext, MessageContext, TopicId @@ -23,7 +23,7 @@ class Feedback: content: str -class ReceiveAgent(TypeRoutedAgent): +class ReceiveAgent(RoutedAgent): def __init__(self) -> None: super().__init__("Receive Agent") @@ -39,7 +39,7 @@ async def on_unhandled_message(self, message: Any, ctx: MessageContext) -> NoRet print(f"Unhandled message: {message}") -class GreeterAgent(TypeRoutedAgent): +class GreeterAgent(RoutedAgent): def __init__(self, receive_agent_id: AgentId) -> None: super().__init__("Greeter Agent") self._receive_agent_id = receive_agent_id diff --git a/python/src/agnext/components/__init__.py b/python/src/agnext/components/__init__.py index b794fdd24d13..17fe80ac23b3 100644 --- a/python/src/agnext/components/__init__.py +++ b/python/src/agnext/components/__init__.py @@ -6,12 +6,13 @@ from ._default_subscription import DefaultSubscription from ._default_topic import DefaultTopicId from ._image import Image -from ._type_routed_agent import TypeRoutedAgent, message_handler +from ._routed_agent import RoutedAgent, message_handler, TypeRoutedAgent from ._type_subscription import TypeSubscription from ._types import FunctionCall __all__ = [ "Image", + "RoutedAgent", "TypeRoutedAgent", "ClosureAgent", "message_handler", diff --git a/python/src/agnext/components/_type_routed_agent.py b/python/src/agnext/components/_routed_agent.py similarity index 92% rename from python/src/agnext/components/_type_routed_agent.py rename to python/src/agnext/components/_routed_agent.py index 0232f27163d1..a3fc984e75b7 100644 --- a/python/src/agnext/components/_type_routed_agent.py +++ b/python/src/agnext/components/_routed_agent.py @@ -1,4 +1,5 @@ import logging +import warnings from functools import wraps from typing import ( Any, @@ -6,7 +7,6 @@ Coroutine, Dict, Literal, - NoReturn, Protocol, Sequence, Type, @@ -127,7 +127,7 @@ async def wrapper(self: Any, message: ReceivesT, ctx: MessageContext) -> Produce raise ValueError("Invalid arguments") -class TypeRoutedAgent(BaseAgent): +class RoutedAgent(BaseAgent): def __init__(self, description: str) -> None: # Self is already bound to the handlers self._handlers: Dict[ @@ -155,7 +155,14 @@ async def on_message(self, message: Any, ctx: MessageContext) -> Any | None: if handler is not None: return await handler(message, ctx) else: - return await self.on_unhandled_message(message, ctx) + return await self.on_unhandled_message(message, ctx) # type: ignore - async def on_unhandled_message(self, message: Any, ctx: MessageContext) -> NoReturn: - raise CantHandleException(f"Unhandled message: {message}") + async def on_unhandled_message(self, message: Any, ctx: MessageContext) -> None: + logger.info(f"Unhandled message: {message}") + + +# Deprecation warning for TypeRoutedAgent +class TypeRoutedAgent(RoutedAgent): + def __init__(self, description: str) -> None: + warnings.warn("TypeRoutedAgent is deprecated. Use RoutedAgent instead.", DeprecationWarning, stacklevel=2) + super().__init__(description) diff --git a/python/src/agnext/components/tool_agent/_tool_agent.py b/python/src/agnext/components/tool_agent/_tool_agent.py index 2f18c1b143b4..b3fa92537fe3 100644 --- a/python/src/agnext/components/tool_agent/_tool_agent.py +++ b/python/src/agnext/components/tool_agent/_tool_agent.py @@ -3,7 +3,7 @@ from typing import List from ...core import MessageContext -from .. import FunctionCall, TypeRoutedAgent, message_handler +from .. import FunctionCall, RoutedAgent, message_handler from ..models import FunctionExecutionResult from ..tools import Tool @@ -37,7 +37,7 @@ class ToolExecutionException(ToolException): pass -class ToolAgent(TypeRoutedAgent): +class ToolAgent(RoutedAgent): """A tool agent accepts direct messages of the type `FunctionCall`, executes the requested tool with the provided arguments, and returns the result as `FunctionExecutionResult` messages. diff --git a/python/teams/team-one/src/team_one/agents/base_agent.py b/python/teams/team-one/src/team_one/agents/base_agent.py index ae5b44ba45ac..4d30a1cb52f2 100644 --- a/python/teams/team-one/src/team_one/agents/base_agent.py +++ b/python/teams/team-one/src/team_one/agents/base_agent.py @@ -3,7 +3,7 @@ from typing import Any from agnext.application.logging import EVENT_LOGGER_NAME -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.core import MessageContext from team_one.messages import ( @@ -18,7 +18,7 @@ logger = logging.getLogger(EVENT_LOGGER_NAME + ".agent") -class TeamOneBaseAgent(TypeRoutedAgent): +class TeamOneBaseAgent(RoutedAgent): """An agent that optionally ensures messages are handled non-concurrently in the order they arrive.""" def __init__( diff --git a/python/teams/team-one/src/team_one/agents/reflex_agents.py b/python/teams/team-one/src/team_one/agents/reflex_agents.py index bdd4511db27d..31d50bde7a07 100644 --- a/python/teams/team-one/src/team_one/agents/reflex_agents.py +++ b/python/teams/team-one/src/team_one/agents/reflex_agents.py @@ -1,11 +1,11 @@ -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.components.models import UserMessage from agnext.core import MessageContext, TopicId from ..messages import BroadcastMessage, RequestReplyMessage -class ReflexAgent(TypeRoutedAgent): +class ReflexAgent(RoutedAgent): def __init__(self, description: str) -> None: super().__init__(description) diff --git a/python/tests/test_cancellation.py b/python/tests/test_cancellation.py index 75d36d1f2881..6dc915fdedb8 100644 --- a/python/tests/test_cancellation.py +++ b/python/tests/test_cancellation.py @@ -3,7 +3,7 @@ import pytest from agnext.application import SingleThreadedAgentRuntime -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.core import AgentId, CancellationToken from agnext.core import MessageContext from agnext.core import AgentInstantiationContext @@ -18,7 +18,7 @@ class MessageType: ... # If you cancel a future, it may not work as you expect. -class LongRunningAgent(TypeRoutedAgent): +class LongRunningAgent(RoutedAgent): def __init__(self) -> None: super().__init__("A long running agent") self.called = False @@ -37,7 +37,7 @@ async def on_new_message(self, message: MessageType, ctx: MessageContext) -> Mes raise -class NestingLongRunningAgent(TypeRoutedAgent): +class NestingLongRunningAgent(RoutedAgent): def __init__(self, nested_agent: AgentId) -> None: super().__init__("A nesting long running agent") self.called = False diff --git a/python/tests/test_routed_agent.py b/python/tests/test_routed_agent.py new file mode 100644 index 000000000000..4d45932e3c12 --- /dev/null +++ b/python/tests/test_routed_agent.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass +import pytest +import logging + +from agnext.application import SingleThreadedAgentRuntime +from agnext.components import TypeSubscription +from agnext.core import TopicId + +from test_utils import LoopbackAgent + + +@dataclass +class UnhandledMessageType: ... + + +@pytest.mark.asyncio +async def test_routed_agent(caplog: pytest.LogCaptureFixture) -> None: + runtime = SingleThreadedAgentRuntime() + with caplog.at_level(logging.INFO): + await runtime.register("loopback", lambda: LoopbackAgent(), lambda: [TypeSubscription("default", "loopback")]) + runtime.start() + await runtime.publish_message(UnhandledMessageType(), topic_id=TopicId("default", "default")) + await runtime.stop_when_idle() + assert any("Unhandled message: " in e.message for e in caplog.records) diff --git a/python/tests/test_types.py b/python/tests/test_types.py index 7298f197a97d..8665c0aa52d9 100644 --- a/python/tests/test_types.py +++ b/python/tests/test_types.py @@ -1,7 +1,7 @@ from types import NoneType from typing import Any, Optional, Union -from agnext.components._type_routed_agent import message_handler +from agnext.components._routed_agent import message_handler from agnext.components._type_helpers import AnyType, get_types from agnext.core import MessageContext diff --git a/python/tests/test_utils/__init__.py b/python/tests/test_utils/__init__.py index 5a056a8911c3..f777932471a8 100644 --- a/python/tests/test_utils/__init__.py +++ b/python/tests/test_utils/__init__.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Any -from agnext.components import TypeRoutedAgent, message_handler +from agnext.components import RoutedAgent, message_handler from agnext.core import BaseAgent from agnext.core import MessageContext @@ -14,7 +14,7 @@ class MessageType: class CascadingMessageType: round: int -class LoopbackAgent(TypeRoutedAgent): +class LoopbackAgent(RoutedAgent): def __init__(self) -> None: super().__init__("A loop back agent.") self.num_calls = 0 @@ -26,7 +26,7 @@ async def on_new_message(self, message: MessageType, ctx: MessageContext) -> Mes return message -class CascadingAgent(TypeRoutedAgent): +class CascadingAgent(RoutedAgent): def __init__(self, max_rounds: int) -> None: super().__init__("A cascading agent.")