autogen/python/docs/src/getting-started/message-and-communication.ipynb

420 lines
14 KiB
Plaintext
Raw Normal View History

{
"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 DefaultSubscription, DefaultTopicId, RoutedAgent, message_handler\n",
"from agnext.core import MessageContext\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",
" await self.publish_message(Message(f\"Publishing a message: {message.content}!\"), topic_id=DefaultTopicId())\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": [
"runtime = SingleThreadedAgentRuntime()\n",
"await runtime.register(\n",
" \"broadcasting_agent\", lambda: BroadcastingAgent(\"Broadcasting Agent\"), lambda: [DefaultSubscription()]\n",
")\n",
"await runtime.register(\"receiving_agent\", lambda: ReceivingAgent(\"Receiving Agent\"), lambda: [DefaultSubscription()])\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(\n",
" \"broadcasting_agent\", lambda: BroadcastingAgent(\"Broadcasting Agent\"), lambda: [DefaultSubscription()]\n",
")\n",
"await runtime.register(\"receiving_agent\", lambda: ReceivingAgent(\"Receiving Agent\"), lambda: [DefaultSubscription()])\n",
"runtime.start()\n",
"await runtime.publish_message(Message(\"Hello, World! From the runtime!\"), topic_id=DefaultTopicId())\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
}