Migrate to using default sub/topic (#403)

This commit is contained in:
Jack Gerrits 2024-08-26 10:30:28 -04:00 committed by GitHub
parent d7ae2038fb
commit dbb35fc335
23 changed files with 491 additions and 526 deletions

View File

@ -1,424 +1,419 @@
{ {
"cells": [ "cells": [
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"# Message and Communication\n", "# Message and Communication\n",
"\n", "\n",
"An agent in AGNext can react to, send, and publish messages,\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", "and messages are the only means through which agents can communicate\n",
"with each other." "with each other."
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## Messages\n", "## Messages\n",
"\n", "\n",
"Messages are serializable objects, they can be defined using:\n", "Messages are serializable objects, they can be defined using:\n",
"\n", "\n",
"- A subclass of Pydantic's {py:class}`pydantic.BaseModel`, or\n", "- A subclass of Pydantic's {py:class}`pydantic.BaseModel`, or\n",
"- A dataclass\n", "- A dataclass\n",
"\n", "\n",
"For example:" "For example:"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 2, "execution_count": 2,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"from dataclasses import dataclass\n", "from dataclasses import dataclass\n",
"\n", "\n",
"\n", "\n",
"@dataclass\n", "@dataclass\n",
"class TextMessage:\n", "class TextMessage:\n",
" content: str\n", " content: str\n",
" source: str\n", " source: str\n",
"\n", "\n",
"\n", "\n",
"@dataclass\n", "@dataclass\n",
"class ImageMessage:\n", "class ImageMessage:\n",
" url: str\n", " url: str\n",
" source: str" " source: str"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"```{note}\n", "```{note}\n",
"Messages are purely data, and should not contain any logic.\n", "Messages are purely data, and should not contain any logic.\n",
"```" "```"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"### Message Handlers\n", "### Message Handlers\n",
"\n", "\n",
"When an agent receives a message the runtime will invoke the agent's message handler\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", "({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", "If this message cannot be handled by the agent, the agent should raise a\n",
"{py:class}`~agnext.core.exceptions.CantHandleException`.\n", "{py:class}`~agnext.core.exceptions.CantHandleException`.\n",
"\n", "\n",
"For convenience, the {py:class}`~agnext.components.RoutedAgent` base class\n", "For convenience, the {py:class}`~agnext.components.RoutedAgent` base class\n",
"provides the {py:meth}`~agnext.components.message_handler` decorator\n", "provides the {py:meth}`~agnext.components.message_handler` decorator\n",
"for associating message types with message handlers,\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", "so developers do not need to implement the {py:meth}`~agnext.core.Agent.on_message` method.\n",
"\n", "\n",
"For example, the following type-routed agent responds to `TextMessage` and `ImageMessage`\n", "For example, the following type-routed agent responds to `TextMessage` and `ImageMessage`\n",
"using different message handlers:" "using different message handlers:"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 4, "execution_count": 4,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"from agnext.application import SingleThreadedAgentRuntime\n", "from agnext.application import SingleThreadedAgentRuntime\n",
"from agnext.components import RoutedAgent, message_handler\n", "from agnext.components import RoutedAgent, message_handler\n",
"from agnext.core import AgentId, MessageContext\n", "from agnext.core import AgentId, MessageContext\n",
"\n", "\n",
"\n", "\n",
"class MyAgent(RoutedAgent):\n", "class MyAgent(RoutedAgent):\n",
" @message_handler\n", " @message_handler\n",
" async def on_text_message(self, message: TextMessage, ctx: MessageContext) -> None:\n", " async def on_text_message(self, message: TextMessage, ctx: MessageContext) -> None:\n",
" print(f\"Hello, {message.source}, you said {message.content}!\")\n", " print(f\"Hello, {message.source}, you said {message.content}!\")\n",
"\n", "\n",
" @message_handler\n", " @message_handler\n",
" async def on_image_message(self, message: ImageMessage, ctx: MessageContext) -> None:\n", " async def on_image_message(self, message: ImageMessage, ctx: MessageContext) -> None:\n",
" print(f\"Hello, {message.source}, you sent me {message.url}!\")" " print(f\"Hello, {message.source}, you sent me {message.url}!\")"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"Create the agent runtime and register the agent (see [Agent and Agent Runtime](agent-and-agent-runtime.ipynb)):" "Create the agent runtime and register the agent (see [Agent and Agent Runtime](agent-and-agent-runtime.ipynb)):"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 6, "execution_count": 6,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"runtime = SingleThreadedAgentRuntime()\n", "runtime = SingleThreadedAgentRuntime()\n",
"await runtime.register(\"my_agent\", lambda: MyAgent(\"My Agent\"))\n", "await runtime.register(\"my_agent\", lambda: MyAgent(\"My Agent\"))\n",
"agent = AgentId(\"my_agent\", \"default\")" "agent = AgentId(\"my_agent\", \"default\")"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"Test this agent with `TextMessage` and `ImageMessage`." "Test this agent with `TextMessage` and `ImageMessage`."
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 7, "execution_count": 7,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"Hello, User, you said Hello, World!!\n", "Hello, User, you said Hello, World!!\n",
"Hello, User, you sent me https://example.com/image.jpg!\n" "Hello, User, you sent me https://example.com/image.jpg!\n"
] ]
} }
], ],
"source": [ "source": [
"runtime.start()\n", "runtime.start()\n",
"await runtime.send_message(TextMessage(content=\"Hello, World!\", source=\"User\"), agent)\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.send_message(ImageMessage(url=\"https://example.com/image.jpg\", source=\"User\"), agent)\n",
"await runtime.stop_when_idle()" "await runtime.stop_when_idle()"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## Communication\n", "## Communication\n",
"\n", "\n",
"There are two types of communication in AGNext:\n", "There are two types of communication in AGNext:\n",
"\n", "\n",
"- **Direct communication**: An agent sends a direct message to another agent.\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", "- **Broadcast communication**: An agent publishes a message to all agents in the same namespace.\n",
"\n", "\n",
"### Direct Communication\n", "### Direct Communication\n",
"\n", "\n",
"To send a direct message to another agent, within a message handler use\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", "the {py:meth}`agnext.core.BaseAgent.send_message` method,\n",
"from the runtime use the {py:meth}`agnext.core.AgentRuntime.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", "Awaiting calls to these methods will return the return value of the\n",
"receiving agent's message handler.\n", "receiving agent's message handler.\n",
"\n", "\n",
"```{note}\n", "```{note}\n",
"If the invoked agent raises an exception while the sender is awaiting,\n", "If the invoked agent raises an exception while the sender is awaiting,\n",
"the exception will be propagated back to the sender.\n", "the exception will be propagated back to the sender.\n",
"```\n", "```\n",
"\n", "\n",
"#### Request/Response\n", "#### Request/Response\n",
"\n", "\n",
"Direct communication can be used for request/response scenarios,\n", "Direct communication can be used for request/response scenarios,\n",
"where the sender expects a response from the receiver.\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", "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", "You can think of this as a function call between agents.\n",
"\n", "\n",
"For example, consider the following type-routed agent:" "For example, consider the following type-routed agent:"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 17, "execution_count": 17,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"from dataclasses import dataclass\n", "from dataclasses import dataclass\n",
"\n", "\n",
"from agnext.application import SingleThreadedAgentRuntime\n", "from agnext.application import SingleThreadedAgentRuntime\n",
"from agnext.components import RoutedAgent, message_handler\n", "from agnext.components import RoutedAgent, message_handler\n",
"from agnext.core import MessageContext\n", "from agnext.core import MessageContext\n",
"\n", "\n",
"\n", "\n",
"@dataclass\n", "@dataclass\n",
"class Message:\n", "class Message:\n",
" content: str\n", " content: str\n",
"\n", "\n",
"\n", "\n",
"class InnerAgent(RoutedAgent):\n", "class InnerAgent(RoutedAgent):\n",
" @message_handler\n", " @message_handler\n",
" async def on_my_message(self, message: Message, ctx: MessageContext) -> Message:\n", " async def on_my_message(self, message: Message, ctx: MessageContext) -> Message:\n",
" return Message(content=f\"Hello from inner, {message.content}\")\n", " return Message(content=f\"Hello from inner, {message.content}\")\n",
"\n", "\n",
"\n", "\n",
"class OuterAgent(RoutedAgent):\n", "class OuterAgent(RoutedAgent):\n",
" def __init__(self, description: str, inner_agent_type: str):\n", " def __init__(self, description: str, inner_agent_type: str):\n",
" super().__init__(description)\n", " super().__init__(description)\n",
" self.inner_agent_id = AgentId(inner_agent_type, self.id.key)\n", " self.inner_agent_id = AgentId(inner_agent_type, self.id.key)\n",
"\n", "\n",
" @message_handler\n", " @message_handler\n",
" async def on_my_message(self, message: Message, ctx: MessageContext) -> None:\n", " async def on_my_message(self, message: Message, ctx: MessageContext) -> None:\n",
" print(f\"Received message: {message.content}\")\n", " print(f\"Received message: {message.content}\")\n",
" # Send a direct message to the inner agent and receves a response.\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", " response = await self.send_message(Message(f\"Hello from outer, {message.content}\"), self.inner_agent_id)\n",
" print(f\"Received inner response: {response.content}\")" " print(f\"Received inner response: {response.content}\")"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"Upone receving a message, the `OuterAgent` sends a direct message to the `InnerAgent` and receives\n", "Upone receving a message, the `OuterAgent` sends a direct message to the `InnerAgent` and receives\n",
"a message in response.\n", "a message in response.\n",
"\n", "\n",
"We can test these agents by sending a `Message` to the `OuterAgent`." "We can test these agents by sending a `Message` to the `OuterAgent`."
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 18, "execution_count": 18,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"Received message: Hello, World!\n", "Received message: Hello, World!\n",
"Received inner response: Hello from inner, Hello from outer, Hello, World!\n" "Received inner response: Hello from inner, Hello from outer, Hello, World!\n"
] ]
} }
], ],
"source": [ "source": [
"runtime = SingleThreadedAgentRuntime()\n", "runtime = SingleThreadedAgentRuntime()\n",
"await runtime.register(\"inner_agent\", lambda: InnerAgent(\"InnerAgent\"))\n", "await runtime.register(\"inner_agent\", lambda: InnerAgent(\"InnerAgent\"))\n",
"await runtime.register(\"outer_agent\", lambda: OuterAgent(\"OuterAgent\", \"InnerAgent\"))\n", "await runtime.register(\"outer_agent\", lambda: OuterAgent(\"OuterAgent\", \"InnerAgent\"))\n",
"runtime.start()\n", "runtime.start()\n",
"outer = AgentId(\"outer_agent\", \"default\")\n", "outer = AgentId(\"outer_agent\", \"default\")\n",
"await runtime.send_message(Message(content=\"Hello, World!\"), outer)\n", "await runtime.send_message(Message(content=\"Hello, World!\"), outer)\n",
"await runtime.stop_when_idle()" "await runtime.stop_when_idle()"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"Both outputs are produced by the `OuterAgent`'s message handler, however the second output is based on the response from the `InnerAgent`." "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", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"### Broadcast Communication\n", "### Broadcast Communication\n",
"\n", "\n",
"Broadcast communication is effectively the publish/subscribe model.\n", "Broadcast communication is effectively the publish/subscribe model.\n",
"As part of the base agent ({py:class}`~agnext.core.BaseAgent`) implementation,\n", "As part of the base agent ({py:class}`~agnext.core.BaseAgent`) implementation,\n",
"it must advertise the message types that\n", "it must advertise the message types that\n",
"it would like to receive when published ({py:attr}`~agnext.core.AgentMetadata.subscriptions`).\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", "If one of these messages is published, the agent's message handler will be invoked.\n",
"\n", "\n",
"The key difference between direct and broadcast communication is that broadcast\n", "The key difference between direct and broadcast communication is that broadcast\n",
"communication cannot be used for request/response scenarios.\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", "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", "from any other agent, even if a receiving agent sends a response.\n",
"\n", "\n",
"```{note}\n", "```{note}\n",
"An agent receiving a message does not know if it is handling a published or direct message.\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", "So, if a response is given to a published message, it will be thrown away.\n",
"```\n", "```\n",
"\n", "\n",
"To publish a message to all agents in the same namespace,\n", "To publish a message to all agents in the same namespace,\n",
"use the {py:meth}`agnext.core.BaseAgent.publish_message` method.\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", "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", "but it will always return `None`.\n",
"If an agent raises an exception while handling a published message,\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", "this will be logged but will not be propagated back to the publishing agent.\n",
"\n", "\n",
"The following example shows a `BroadcastingAgent` that publishes a message\n", "The following example shows a `BroadcastingAgent` that publishes a message\n",
"upong receiving a message. A `ReceivingAgent` that prints the message\n", "upong receiving a message. A `ReceivingAgent` that prints the message\n",
"it receives." "it receives."
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 20, "execution_count": 20,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"from agnext.application import SingleThreadedAgentRuntime\n", "from agnext.application import SingleThreadedAgentRuntime\n",
"from agnext.components import RoutedAgent, message_handler\n", "from agnext.components import DefaultSubscription, DefaultTopicId, RoutedAgent, message_handler\n",
"from agnext.core import MessageContext, TopicId\n", "from agnext.core import MessageContext\n",
"\n", "\n",
"\n", "\n",
"class BroadcastingAgent(RoutedAgent):\n", "class BroadcastingAgent(RoutedAgent):\n",
" @message_handler\n", " @message_handler\n",
" async def on_my_message(self, message: Message, ctx: MessageContext) -> None:\n", " async def on_my_message(self, message: Message, ctx: MessageContext) -> None:\n",
" # Publish a message to all agents in the same namespace.\n", " # Publish a message to all agents in the same namespace.\n",
" assert ctx.topic_id is not None\n", " await self.publish_message(Message(f\"Publishing a message: {message.content}!\"), topic_id=DefaultTopicId())\n",
" await self.publish_message(\n", "\n",
" Message(f\"Publishing a message: {message.content}!\"), topic_id=TopicId(\"deafult\", self.id.key)\n", "\n",
" )\n", "class ReceivingAgent(RoutedAgent):\n",
"\n", " @message_handler\n",
"\n", " async def on_my_message(self, message: Message, ctx: MessageContext) -> None:\n",
"class ReceivingAgent(RoutedAgent):\n", " print(f\"Received a message: {message.content}\")"
" @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": [
"cell_type": "markdown", "Sending a direct message to the `BroadcastingAgent` will result in a message being published by\n",
"metadata": {}, "the `BroadcastingAgent` and received by the `ReceivingAgent`."
"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": {},
"cell_type": "code", "outputs": [
"execution_count": 21, {
"metadata": {}, "name": "stdout",
"outputs": [ "output_type": "stream",
{ "text": [
"name": "stdout", "Received a message: Publishing a message: Hello, World!!\n"
"output_type": "stream", ]
"text": [ }
"Received a message: Publishing a message: Hello, World!!\n" ],
] "source": [
} "runtime = SingleThreadedAgentRuntime()\n",
], "await runtime.register(\n",
"source": [ " \"broadcasting_agent\", lambda: BroadcastingAgent(\"Broadcasting Agent\"), lambda: [DefaultSubscription()]\n",
"from agnext.components import TypeSubscription\n", ")\n",
"\n", "await runtime.register(\"receiving_agent\", lambda: ReceivingAgent(\"Receiving Agent\"), lambda: [DefaultSubscription()])\n",
"runtime = SingleThreadedAgentRuntime()\n", "runtime.start()\n",
"await runtime.register(\"broadcasting_agent\", lambda: BroadcastingAgent(\"Broadcasting Agent\"))\n", "await runtime.send_message(Message(\"Hello, World!\"), AgentId(\"broadcasting_agent\", \"default\"))\n",
"await runtime.register(\"receiving_agent\", lambda: ReceivingAgent(\"Receiving Agent\"))\n", "await runtime.stop()"
"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", "cell_type": "markdown",
"await runtime.stop()" "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",
"cell_type": "markdown", "{py:meth}`agnext.core.AgentRuntime.publish_message` method."
"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", "cell_type": "code",
"{py:meth}`agnext.core.AgentRuntime.publish_message` method." "execution_count": 22,
] "metadata": {},
}, "outputs": [
{ {
"cell_type": "code", "name": "stdout",
"execution_count": 22, "output_type": "stream",
"metadata": {}, "text": [
"outputs": [ "Received a message: Hello, World! From the runtime!\n",
{ "Received a message: Publishing a message: Hello, World! From the runtime!!\n"
"name": "stdout", ]
"output_type": "stream", }
"text": [ ],
"Received a message: Hello, World! From the runtime!\n", "source": [
"Received a message: Publishing a message: Hello, World! From the runtime!!\n" "# Replace send_message with publish_message in the above example.\n",
] "\n",
} "runtime = SingleThreadedAgentRuntime()\n",
], "await runtime.register(\n",
"source": [ " \"broadcasting_agent\", lambda: BroadcastingAgent(\"Broadcasting Agent\"), lambda: [DefaultSubscription()]\n",
"# Replace send_message with publish_message in the above example.\n", ")\n",
"\n", "await runtime.register(\"receiving_agent\", lambda: ReceivingAgent(\"Receiving Agent\"), lambda: [DefaultSubscription()])\n",
"runtime = SingleThreadedAgentRuntime()\n", "runtime.start()\n",
"await runtime.register(\"broadcasting_agent\", lambda: BroadcastingAgent(\"Broadcasting Agent\"))\n", "await runtime.publish_message(Message(\"Hello, World! From the runtime!\"), topic_id=DefaultTopicId())\n",
"await runtime.register(\"receiving_agent\", lambda: ReceivingAgent(\"Receiving Agent\"))\n", "await runtime.stop_when_idle()"
"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", "cell_type": "markdown",
"await runtime.stop_when_idle()" "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",
"cell_type": "markdown", "a message published by the `BroadcastingAgent`.\n",
"metadata": {}, "\n",
"source": [ "```{note}\n",
"The first output is from the `ReceivingAgent` that received a message published\n", "If an agent publishes a message type for which it is subscribed it will not\n",
"by the runtime. The second output is from the `ReceivingAgent` that received\n", "receive the message it published. This is to prevent infinite loops.\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"
"metadata": { },
"kernelspec": { "language_info": {
"display_name": "agnext", "codemirror_mode": {
"language": "python", "name": "ipython",
"name": "python3" "version": 3
}, },
"language_info": { "file_extension": ".py",
"codemirror_mode": { "mimetype": "text/x-python",
"name": "ipython", "name": "python",
"version": 3 "nbconvert_exporter": "python",
}, "pygments_lexer": "ipython3",
"file_extension": ".py", "version": "3.11.9"
"mimetype": "text/x-python", }
"name": "python", },
"nbconvert_exporter": "python", "nbformat": 4,
"pygments_lexer": "ipython3", "nbformat_minor": 2
"version": "3.11.9" }
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@ -3,6 +3,7 @@ import json
from typing import Any, Coroutine, Dict, List, Mapping, Sequence, Tuple from typing import Any, Coroutine, Dict, List, Mapping, Sequence, Tuple
from agnext.components import ( from agnext.components import (
DefaultTopicId,
FunctionCall, FunctionCall,
RoutedAgent, RoutedAgent,
message_handler, message_handler,
@ -110,9 +111,8 @@ class ChatCompletionAgent(RoutedAgent):
# Generate a response. # Generate a response.
response = await self._generate_response(message.response_format, ctx) response = await self._generate_response(message.response_format, ctx)
assert ctx.topic_id is not None
# Publish the response. # Publish the response.
await self.publish_message(response, topic_id=ctx.topic_id) await self.publish_message(response, topic_id=DefaultTopicId())
@message_handler() @message_handler()
async def on_tool_call_message( async def on_tool_call_message(

View File

@ -2,6 +2,7 @@ from typing import Literal
import openai import openai
from agnext.components import ( from agnext.components import (
DefaultTopicId,
Image, Image,
RoutedAgent, RoutedAgent,
message_handler, message_handler,
@ -57,8 +58,7 @@ class ImageGenerationAgent(RoutedAgent):
image is published as a MultiModalMessage.""" image is published as a MultiModalMessage."""
response = await self._generate_response(ctx.cancellation_token) response = await self._generate_response(ctx.cancellation_token)
assert ctx.topic_id is not None await self.publish_message(response, topic_id=DefaultTopicId())
await self.publish_message(response, topic_id=ctx.topic_id)
async def _generate_response(self, cancellation_token: CancellationToken) -> MultiModalMessage: async def _generate_response(self, cancellation_token: CancellationToken) -> MultiModalMessage:
messages = await self._memory.get_messages() messages = await self._memory.get_messages()

View File

@ -1,7 +1,7 @@
from typing import Any, Callable, List, Mapping from typing import Any, Callable, List, Mapping
import openai import openai
from agnext.components import RoutedAgent, message_handler from agnext.components import DefaultTopicId, RoutedAgent, message_handler
from agnext.core import ( from agnext.core import (
CancellationToken, CancellationToken,
MessageContext, # type: ignore MessageContext, # type: ignore
@ -80,8 +80,7 @@ class OpenAIAssistantAgent(RoutedAgent):
async def on_publish_now(self, message: PublishNow, ctx: MessageContext) -> None: async def on_publish_now(self, message: PublishNow, ctx: MessageContext) -> None:
"""Handle a publish now message. This method generates a response and publishes it.""" """Handle a publish now message. This method generates a response and publishes it."""
response = await self._generate_response(message.response_format, ctx.cancellation_token) response = await self._generate_response(message.response_format, ctx.cancellation_token)
assert ctx.topic_id is not None await self.publish_message(response, DefaultTopicId())
await self.publish_message(response, ctx.topic_id)
async def _generate_response( async def _generate_response(
self, self,

View File

@ -1,6 +1,6 @@
import asyncio import asyncio
from agnext.components import RoutedAgent, message_handler from agnext.components import DefaultTopicId, RoutedAgent, message_handler
from agnext.core import MessageContext from agnext.core import MessageContext
from ..types import PublishNow, TextMessage from ..types import PublishNow, TextMessage
@ -23,8 +23,9 @@ class UserProxyAgent(RoutedAgent):
async def on_publish_now(self, message: PublishNow, ctx: MessageContext) -> None: async def on_publish_now(self, message: PublishNow, ctx: MessageContext) -> None:
"""Handle a publish now message. This method prompts the user for input, then publishes it.""" """Handle a publish now message. This method prompts the user for input, then publishes it."""
user_input = await self.get_user_input(self._user_input_prompt) user_input = await self.get_user_input(self._user_input_prompt)
assert ctx.topic_id is not None await self.publish_message(
await self.publish_message(TextMessage(content=user_input, source=self.metadata["type"]), topic_id=ctx.topic_id) TextMessage(content=user_input, source=self.metadata["type"]), topic_id=DefaultTopicId()
)
async def get_user_input(self, prompt: str) -> str: async def get_user_input(self, prompt: str) -> str:
"""Get user input from the console. Override this method to customize how user input is retrieved.""" """Get user input from the console. Override this method to customize how user input is retrieved."""

View File

@ -17,7 +17,7 @@ from dataclasses import dataclass
from typing import List from typing import List
from agnext.application import SingleThreadedAgentRuntime from agnext.application import SingleThreadedAgentRuntime
from agnext.components import RoutedAgent, message_handler from agnext.components import DefaultTopicId, RoutedAgent, message_handler
from agnext.components._type_subscription import TypeSubscription from agnext.components._type_subscription import TypeSubscription
from agnext.components.models import ( from agnext.components.models import (
AssistantMessage, AssistantMessage,
@ -74,7 +74,7 @@ class ChatCompletionAgent(RoutedAgent):
if ctx.topic_id is not None: if ctx.topic_id is not None:
await self.publish_message( await self.publish_message(
Message(content=response.content, source=self.metadata["type"]), topic_id=ctx.topic_id Message(content=response.content, source=self.metadata["type"]), topic_id=DefaultTopicId()
) )

View File

@ -12,7 +12,7 @@ from typing import List
import aiofiles import aiofiles
import openai import openai
from agnext.application import SingleThreadedAgentRuntime from agnext.application import SingleThreadedAgentRuntime
from agnext.components import RoutedAgent, message_handler from agnext.components import DefaultTopicId, RoutedAgent, message_handler
from agnext.core import AgentId, AgentRuntime, MessageContext from agnext.core import AgentId, AgentRuntime, MessageContext
from openai import AsyncAssistantEventHandler from openai import AsyncAssistantEventHandler
from openai.types.beta.thread import ToolResources from openai.types.beta.thread import ToolResources
@ -109,9 +109,8 @@ class UserProxyAgent(RoutedAgent):
return return
else: else:
# Publish user input and exit handler. # Publish user input and exit handler.
assert ctx.topic_id is not None
await self.publish_message( await self.publish_message(
TextMessage(content=user_input, source=self.metadata["type"]), topic_id=ctx.topic_id TextMessage(content=user_input, source=self.metadata["type"]), topic_id=DefaultTopicId()
) )
return return

View File

@ -6,7 +6,8 @@ import os
import sys import sys
from agnext.application import SingleThreadedAgentRuntime from agnext.application import SingleThreadedAgentRuntime
from agnext.components import RoutedAgent, message_handler from agnext.components import DefaultTopicId, RoutedAgent, message_handler
from agnext.components._default_subscription import DefaultSubscription
from agnext.components.memory import ChatMemory from agnext.components.memory import ChatMemory
from agnext.components.models import ChatCompletionClient, SystemMessage from agnext.components.models import ChatCompletionClient, SystemMessage
from agnext.core import AgentId, AgentInstantiationContext, AgentProxy, AgentRuntime from agnext.core import AgentId, AgentInstantiationContext, AgentProxy, AgentRuntime
@ -76,9 +77,8 @@ Use the following JSON format to provide your thought on the latest message and
# Publish the response if needed. # Publish the response if needed.
if respond is True or str(respond).lower().strip() == "true": if respond is True or str(respond).lower().strip() == "true":
assert ctx.topic_id is not None
await self.publish_message( await self.publish_message(
TextMessage(source=self.metadata["type"], content=str(response)), topic_id=ctx.topic_id TextMessage(source=self.metadata["type"], content=str(response)), topic_id=DefaultTopicId()
) )
@ -98,6 +98,7 @@ async def chat_room(runtime: AgentRuntime, app: TextualChatApp) -> None:
description="The user in the chat room.", description="The user in the chat room.",
app=app, app=app,
), ),
lambda: [DefaultSubscription()],
) )
await runtime.register( await runtime.register(
"Alice", "Alice",
@ -108,6 +109,7 @@ async def chat_room(runtime: AgentRuntime, app: TextualChatApp) -> None:
memory=BufferedChatMemory(buffer_size=10), memory=BufferedChatMemory(buffer_size=10),
model_client=get_chat_completion_client_from_envs(model="gpt-4-turbo"), model_client=get_chat_completion_client_from_envs(model="gpt-4-turbo"),
), ),
lambda: [DefaultSubscription()],
) )
alice = AgentProxy(AgentId("Alice", "default"), runtime) alice = AgentProxy(AgentId("Alice", "default"), runtime)
await runtime.register( await runtime.register(
@ -119,6 +121,7 @@ async def chat_room(runtime: AgentRuntime, app: TextualChatApp) -> None:
memory=BufferedChatMemory(buffer_size=10), memory=BufferedChatMemory(buffer_size=10),
model_client=get_chat_completion_client_from_envs(model="gpt-4-turbo"), model_client=get_chat_completion_client_from_envs(model="gpt-4-turbo"),
), ),
lambda: [DefaultSubscription()],
) )
bob = AgentProxy(AgentId("Bob", "default"), runtime) bob = AgentProxy(AgentId("Bob", "default"), runtime)
await runtime.register( await runtime.register(
@ -130,6 +133,7 @@ async def chat_room(runtime: AgentRuntime, app: TextualChatApp) -> None:
memory=BufferedChatMemory(buffer_size=10), memory=BufferedChatMemory(buffer_size=10),
model_client=get_chat_completion_client_from_envs(model="gpt-4-turbo"), model_client=get_chat_completion_client_from_envs(model="gpt-4-turbo"),
), ),
lambda: [DefaultSubscription()],
) )
charlie = AgentProxy(AgentId("Charlie", "default"), runtime) charlie = AgentProxy(AgentId("Charlie", "default"), runtime)
app.welcoming_notice = f"""Welcome to the chat room demo with the following participants: app.welcoming_notice = f"""Welcome to the chat room demo with the following participants:

View File

@ -10,10 +10,11 @@ import sys
from typing import Annotated, Literal from typing import Annotated, Literal
from agnext.application import SingleThreadedAgentRuntime from agnext.application import SingleThreadedAgentRuntime
from agnext.components import DefaultTopicId
from agnext.components._type_subscription import TypeSubscription from agnext.components._type_subscription import TypeSubscription
from agnext.components.models import SystemMessage from agnext.components.models import SystemMessage
from agnext.components.tools import FunctionTool from agnext.components.tools import FunctionTool
from agnext.core import AgentInstantiationContext, AgentRuntime, TopicId from agnext.core import AgentInstantiationContext, AgentRuntime
from chess import BLACK, SQUARE_NAMES, WHITE, Board, Move from chess import BLACK, SQUARE_NAMES, WHITE, Board, Move
from chess import piece_name as get_piece_name from chess import piece_name as get_piece_name
@ -214,9 +215,7 @@ async def main() -> None:
await chess_game(runtime) await chess_game(runtime)
runtime.start() runtime.start()
# Publish an initial message to trigger the group chat manager to start orchestration. # Publish an initial message to trigger the group chat manager to start orchestration.
await runtime.publish_message( await runtime.publish_message(TextMessage(content="Game started.", source="System"), topic_id=DefaultTopicId())
TextMessage(content="Game started.", source="System"), topic_id=TopicId("default", "default")
)
await runtime.stop_when_idle() await runtime.stop_when_idle()

View File

@ -4,7 +4,7 @@ import random
import sys import sys
from asyncio import Future from asyncio import Future
from agnext.components import Image, RoutedAgent, message_handler from agnext.components import DefaultTopicId, Image, RoutedAgent, message_handler
from agnext.core import AgentRuntime, CancellationToken from agnext.core import AgentRuntime, CancellationToken
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.containers import ScrollableContainer from textual.containers import ScrollableContainer
@ -13,7 +13,6 @@ from textual_imageview.viewer import ImageViewer
sys.path.append(os.path.join(os.path.dirname(__file__), "..")) sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
from agnext.core import TopicId
from common.types import ( from common.types import (
MultiModalMessage, MultiModalMessage,
PublishNow, PublishNow,
@ -136,9 +135,7 @@ class TextualChatApp(App): # type: ignore
chat_messages.query("#typing").remove() chat_messages.query("#typing").remove()
# Publish the user message to the runtime. # Publish the user message to the runtime.
await self._runtime.publish_message( await self._runtime.publish_message(
# TODO fix hard coded topic_id TextMessage(source=self._user_name, content=user_input), topic_id=DefaultTopicId()
TextMessage(source=self._user_name, content=user_input),
topic_id=TopicId("default", "default"),
) )
async def post_runtime_message(self, message: TextMessage | MultiModalMessage) -> None: # type: ignore async def post_runtime_message(self, message: TextMessage | MultiModalMessage) -> None: # type: ignore

View File

@ -1,4 +1,4 @@
from agnext.components import RoutedAgent, message_handler from agnext.components import DefaultTopicId, RoutedAgent, message_handler
from agnext.components.models import ChatCompletionClient from agnext.components.models import ChatCompletionClient
from agnext.components.models._types import SystemMessage from agnext.components.models._types import SystemMessage
from agnext.core import MessageContext from agnext.core import MessageContext
@ -30,7 +30,6 @@ class AuditAgent(RoutedAgent):
assert isinstance(completion.content, str) assert isinstance(completion.content, str)
if "NOTFORME" in completion.content: if "NOTFORME" in completion.content:
return return
assert ctx.topic_id is not None
await self.publish_message( await self.publish_message(
AuditorAlert(UserId=message.UserId, auditorAlertMessage=completion.content), topic_id=ctx.topic_id AuditorAlert(UserId=message.UserId, auditorAlertMessage=completion.content), topic_id=DefaultTopicId()
) )

View File

@ -3,6 +3,7 @@ from typing import Literal
import openai import openai
from agnext.components import ( from agnext.components import (
DefaultTopicId,
RoutedAgent, RoutedAgent,
message_handler, message_handler,
) )
@ -33,9 +34,8 @@ class GraphicDesignerAgent(RoutedAgent):
image_uri = response.data[0].url image_uri = response.data[0].url
logger.info(f"Generated image for article. Got response: '{image_uri}'") logger.info(f"Generated image for article. Got response: '{image_uri}'")
assert ctx.topic_id is not None
await self.publish_message( await self.publish_message(
GraphicDesignCreated(UserId=message.UserId, imageUri=image_uri), topic_id=ctx.topic_id GraphicDesignCreated(UserId=message.UserId, imageUri=image_uri), topic_id=DefaultTopicId()
) )
except Exception as e: except Exception as e:
logger.error(f"Failed to generate image for article. Error: {e}") logger.error(f"Failed to generate image for article. Error: {e}")

View File

@ -2,8 +2,8 @@ import asyncio
import os import os
from agnext.application import SingleThreadedAgentRuntime from agnext.application import SingleThreadedAgentRuntime
from agnext.components import Image, RoutedAgent, message_handler from agnext.components import DefaultTopicId, Image, RoutedAgent, message_handler
from agnext.core import MessageContext, TopicId from agnext.core import MessageContext
from app import build_app from app import build_app
from dotenv import load_dotenv from dotenv import load_dotenv
from messages import ArticleCreated, AuditorAlert, AuditText, GraphicDesignCreated from messages import ArticleCreated, AuditorAlert, AuditText, GraphicDesignCreated
@ -34,15 +34,13 @@ async def main() -> None:
runtime.start() runtime.start()
topic_id = TopicId("default", "default")
await runtime.publish_message( await runtime.publish_message(
AuditText(text="Buy my product for a MASSIVE 50% discount.", UserId="user-1"), topic_id=topic_id AuditText(text="Buy my product for a MASSIVE 50% discount.", UserId="user-1"), topic_id=DefaultTopicId()
) )
await runtime.publish_message( await runtime.publish_message(
ArticleCreated(article="The best article ever written about trees and rocks", UserId="user-2"), ArticleCreated(article="The best article ever written about trees and rocks", UserId="user-2"),
topic_id=topic_id, topic_id=DefaultTopicId(),
) )
await runtime.stop_when_idle() await runtime.stop_when_idle()

View File

@ -21,7 +21,7 @@ from dataclasses import dataclass
from typing import Dict, List from typing import Dict, List
from agnext.application import SingleThreadedAgentRuntime from agnext.application import SingleThreadedAgentRuntime
from agnext.components import RoutedAgent, message_handler from agnext.components import DefaultTopicId, RoutedAgent, message_handler
from agnext.components._type_subscription import TypeSubscription from agnext.components._type_subscription import TypeSubscription
from agnext.components.code_executor import CodeBlock, CodeExecutor, LocalCommandLineCodeExecutor from agnext.components.code_executor import CodeBlock, CodeExecutor, LocalCommandLineCodeExecutor
from agnext.components.models import ( from agnext.components.models import (
@ -102,12 +102,11 @@ Reply "TERMINATE" in the end when everything is done."""
AssistantMessage(content=response.content, source=self.metadata["type"]) AssistantMessage(content=response.content, source=self.metadata["type"])
) )
assert ctx.topic_id is not None
# Publish the code execution task. # Publish the code execution task.
await self.publish_message( await self.publish_message(
CodeExecutionTask(content=response.content, session_id=session_id), CodeExecutionTask(content=response.content, session_id=session_id),
cancellation_token=ctx.cancellation_token, cancellation_token=ctx.cancellation_token,
topic_id=ctx.topic_id, topic_id=DefaultTopicId(),
) )
@message_handler @message_handler
@ -124,11 +123,10 @@ Reply "TERMINATE" in the end when everything is done."""
if "TERMINATE" in response.content: if "TERMINATE" in response.content:
# If the task is completed, publish a message with the completion content. # If the task is completed, publish a message with the completion content.
assert ctx.topic_id is not None
await self.publish_message( await self.publish_message(
TaskCompletion(content=response.content), TaskCompletion(content=response.content),
cancellation_token=ctx.cancellation_token, cancellation_token=ctx.cancellation_token,
topic_id=ctx.topic_id, topic_id=DefaultTopicId(),
) )
print("--------------------") print("--------------------")
print("Task completed:") print("Task completed:")
@ -136,11 +134,10 @@ Reply "TERMINATE" in the end when everything is done."""
return return
# Publish the code execution task. # Publish the code execution task.
assert ctx.topic_id is not None
await self.publish_message( await self.publish_message(
CodeExecutionTask(content=response.content, session_id=message.session_id), CodeExecutionTask(content=response.content, session_id=message.session_id),
cancellation_token=ctx.cancellation_token, cancellation_token=ctx.cancellation_token,
topic_id=ctx.topic_id, topic_id=DefaultTopicId(),
) )
@ -157,13 +154,12 @@ class Executor(RoutedAgent):
code_blocks = self._extract_code_blocks(message.content) code_blocks = self._extract_code_blocks(message.content)
if not code_blocks: if not code_blocks:
# If no code block is found, publish a message with an error. # If no code block is found, publish a message with an error.
assert ctx.topic_id is not None
await self.publish_message( await self.publish_message(
CodeExecutionTaskResult( CodeExecutionTaskResult(
output="Error: no Markdown code block found.", exit_code=1, session_id=message.session_id output="Error: no Markdown code block found.", exit_code=1, session_id=message.session_id
), ),
cancellation_token=ctx.cancellation_token, cancellation_token=ctx.cancellation_token,
topic_id=ctx.topic_id, topic_id=DefaultTopicId(),
) )
return return
# Execute code blocks. # Execute code blocks.
@ -171,11 +167,10 @@ class Executor(RoutedAgent):
code_blocks=code_blocks, cancellation_token=ctx.cancellation_token code_blocks=code_blocks, cancellation_token=ctx.cancellation_token
) )
# Publish the code execution result. # Publish the code execution result.
assert ctx.topic_id is not None
await self.publish_message( await self.publish_message(
CodeExecutionTaskResult(output=result.output, exit_code=result.exit_code, session_id=message.session_id), CodeExecutionTaskResult(output=result.output, exit_code=result.exit_code, session_id=message.session_id),
cancellation_token=ctx.cancellation_token, cancellation_token=ctx.cancellation_token,
topic_id=ctx.topic_id, topic_id=DefaultTopicId(),
) )
def _extract_code_blocks(self, markdown_text: str) -> List[CodeBlock]: def _extract_code_blocks(self, markdown_text: str) -> List[CodeBlock]:

View File

@ -21,7 +21,7 @@ from dataclasses import dataclass
from typing import Dict, List, Union from typing import Dict, List, Union
from agnext.application import SingleThreadedAgentRuntime from agnext.application import SingleThreadedAgentRuntime
from agnext.components import RoutedAgent, message_handler from agnext.components import DefaultTopicId, RoutedAgent, message_handler
from agnext.components._type_subscription import TypeSubscription from agnext.components._type_subscription import TypeSubscription
from agnext.components.models import ( from agnext.components.models import (
AssistantMessage, AssistantMessage,
@ -112,14 +112,13 @@ Please review the code and provide feedback.
review_text = "Code review:\n" + "\n".join([f"{k}: {v}" for k, v in review.items()]) review_text = "Code review:\n" + "\n".join([f"{k}: {v}" for k, v in review.items()])
approved = review["approval"].lower().strip() == "approve" approved = review["approval"].lower().strip() == "approve"
# Publish the review result. # Publish the review result.
assert ctx.topic_id is not None
await self.publish_message( await self.publish_message(
CodeReviewResult( CodeReviewResult(
review=review_text, review=review_text,
approved=approved, approved=approved,
session_id=message.session_id, session_id=message.session_id,
), ),
topic_id=ctx.topic_id, topic_id=DefaultTopicId(),
) )
@ -183,10 +182,9 @@ Code: <Your code>
# Store the code review task in the session memory. # Store the code review task in the session memory.
self._session_memory[session_id].append(code_review_task) self._session_memory[session_id].append(code_review_task)
# Publish a code review task. # Publish a code review task.
assert ctx.topic_id is not None
await self.publish_message( await self.publish_message(
code_review_task, code_review_task,
topic_id=ctx.topic_id, topic_id=DefaultTopicId(),
) )
@message_handler @message_handler
@ -201,14 +199,13 @@ Code: <Your code>
# Check if the code is approved. # Check if the code is approved.
if message.approved: if message.approved:
# Publish the code writing result. # Publish the code writing result.
assert ctx.topic_id is not None
await self.publish_message( await self.publish_message(
CodeWritingResult( CodeWritingResult(
code=review_request.code, code=review_request.code,
task=review_request.code_writing_task, task=review_request.code_writing_task,
review=message.review, review=message.review,
), ),
topic_id=ctx.topic_id, topic_id=DefaultTopicId(),
) )
print("Code Writing Result:") print("Code Writing Result:")
print("-" * 80) print("-" * 80)
@ -247,10 +244,9 @@ Code: <Your code>
# Store the code review task in the session memory. # Store the code review task in the session memory.
self._session_memory[message.session_id].append(code_review_task) self._session_memory[message.session_id].append(code_review_task)
# Publish a new code review task. # Publish a new code review task.
assert ctx.topic_id is not None
await self.publish_message( await self.publish_message(
code_review_task, code_review_task,
topic_id=ctx.topic_id, topic_id=DefaultTopicId(),
) )
def _extract_code_block(self, markdown_text: str) -> Union[str, None]: def _extract_code_block(self, markdown_text: str) -> Union[str, None]:

View File

@ -18,7 +18,7 @@ from dataclasses import dataclass
from typing import List from typing import List
from agnext.application import SingleThreadedAgentRuntime from agnext.application import SingleThreadedAgentRuntime
from agnext.components import RoutedAgent, message_handler from agnext.components import DefaultTopicId, RoutedAgent, message_handler
from agnext.components.models import ( from agnext.components.models import (
AssistantMessage, AssistantMessage,
ChatCompletionClient, ChatCompletionClient,
@ -69,8 +69,7 @@ class RoundRobinGroupChatManager(RoutedAgent):
self._round_count += 1 self._round_count += 1
if self._round_count > self._num_rounds * len(self._participants): if self._round_count > self._num_rounds * len(self._participants):
# End the conversation after the specified number of rounds. # End the conversation after the specified number of rounds.
assert ctx.topic_id is not None await self.publish_message(Termination(), DefaultTopicId())
await self.publish_message(Termination(), ctx.topic_id)
return return
# Send a request to speak message to the selected speaker. # Send a request to speak message to the selected speaker.
await self.send_message(RequestToSpeak(), speaker) await self.send_message(RequestToSpeak(), speaker)
@ -107,8 +106,7 @@ class GroupChatParticipant(RoutedAgent):
assert isinstance(response.content, str) assert isinstance(response.content, str)
speech = Message(content=response.content, source=self.metadata["type"]) speech = Message(content=response.content, source=self.metadata["type"])
self._memory.append(speech) self._memory.append(speech)
assert ctx.topic_id is not None await self.publish_message(speech, topic_id=DefaultTopicId())
await self.publish_message(speech, topic_id=ctx.topic_id)
async def main() -> None: async def main() -> None:

View File

@ -15,7 +15,7 @@ from dataclasses import dataclass
from typing import Dict, List from typing import Dict, List
from agnext.application import SingleThreadedAgentRuntime from agnext.application import SingleThreadedAgentRuntime
from agnext.components import RoutedAgent, message_handler from agnext.components import DefaultTopicId, RoutedAgent, message_handler
from agnext.components._type_subscription import TypeSubscription from agnext.components._type_subscription import TypeSubscription
from agnext.components.models import ChatCompletionClient, SystemMessage, UserMessage from agnext.components.models import ChatCompletionClient, SystemMessage, UserMessage
from agnext.core import MessageContext from agnext.core import MessageContext
@ -68,8 +68,7 @@ class ReferenceAgent(RoutedAgent):
response = await self._model_client.create(self._system_messages + [task_message]) response = await self._model_client.create(self._system_messages + [task_message])
assert isinstance(response.content, str) assert isinstance(response.content, str)
task_result = ReferenceAgentTaskResult(session_id=message.session_id, result=response.content) task_result = ReferenceAgentTaskResult(session_id=message.session_id, result=response.content)
assert ctx.topic_id is not None await self.publish_message(task_result, topic_id=DefaultTopicId())
await self.publish_message(task_result, topic_id=ctx.topic_id)
class AggregatorAgent(RoutedAgent): class AggregatorAgent(RoutedAgent):
@ -93,8 +92,7 @@ class AggregatorAgent(RoutedAgent):
"""Handle a task message. This method publishes the task to the reference agents.""" """Handle a task message. This method publishes the task to the reference agents."""
session_id = str(uuid.uuid4()) session_id = str(uuid.uuid4())
ref_task = ReferenceAgentTask(session_id=session_id, task=message.task) ref_task = ReferenceAgentTask(session_id=session_id, task=message.task)
assert ctx.topic_id is not None await self.publish_message(ref_task, topic_id=DefaultTopicId())
await self.publish_message(ref_task, topic_id=ctx.topic_id)
@message_handler @message_handler
async def handle_result(self, message: ReferenceAgentTaskResult, ctx: MessageContext) -> None: async def handle_result(self, message: ReferenceAgentTaskResult, ctx: MessageContext) -> None:
@ -108,8 +106,7 @@ class AggregatorAgent(RoutedAgent):
) )
assert isinstance(response.content, str) assert isinstance(response.content, str)
task_result = AggregatorTaskResult(result=response.content) task_result = AggregatorTaskResult(result=response.content)
assert ctx.topic_id is not None await self.publish_message(task_result, topic_id=DefaultTopicId())
await self.publish_message(task_result, topic_id=ctx.topic_id)
self._session_results.pop(message.session_id) self._session_results.pop(message.session_id)
print(f"Aggregator result: {response.content}") print(f"Aggregator result: {response.content}")

View File

@ -40,7 +40,7 @@ from dataclasses import dataclass
from typing import Dict, List, Tuple from typing import Dict, List, Tuple
from agnext.application import SingleThreadedAgentRuntime from agnext.application import SingleThreadedAgentRuntime
from agnext.components import RoutedAgent, message_handler from agnext.components import DefaultTopicId, RoutedAgent, message_handler
from agnext.components._type_subscription import TypeSubscription from agnext.components._type_subscription import TypeSubscription
from agnext.components.models import ( from agnext.components.models import (
AssistantMessage, AssistantMessage,
@ -165,11 +165,10 @@ class MathSolver(RoutedAgent):
answer = match.group(1) answer = match.group(1)
# Increment the counter. # Increment the counter.
self._counters[message.session_id] = self._counters.get(message.session_id, 0) + 1 self._counters[message.session_id] = self._counters.get(message.session_id, 0) + 1
assert ctx.topic_id is not None
if self._counters[message.session_id] == self._max_round: if self._counters[message.session_id] == self._max_round:
# If the counter reaches the maximum round, publishes a final response. # If the counter reaches the maximum round, publishes a final response.
await self.publish_message( await self.publish_message(
FinalSolverResponse(answer=answer, session_id=message.session_id), topic_id=ctx.topic_id FinalSolverResponse(answer=answer, session_id=message.session_id), topic_id=DefaultTopicId()
) )
else: else:
# Publish intermediate response. # Publish intermediate response.
@ -181,7 +180,7 @@ class MathSolver(RoutedAgent):
session_id=message.session_id, session_id=message.session_id,
round=self._counters[message.session_id], round=self._counters[message.session_id],
), ),
topic_id=ctx.topic_id, topic_id=DefaultTopicId(),
) )
@ -199,9 +198,8 @@ class MathAggregator(RoutedAgent):
"in the form of {{answer}}, at the end of your response." "in the form of {{answer}}, at the end of your response."
) )
session_id = str(uuid.uuid4()) session_id = str(uuid.uuid4())
assert ctx.topic_id is not None
await self.publish_message( await self.publish_message(
SolverRequest(content=prompt, session_id=session_id, question=message.content), topic_id=ctx.topic_id SolverRequest(content=prompt, session_id=session_id, question=message.content), topic_id=DefaultTopicId()
) )
@message_handler @message_handler
@ -212,8 +210,7 @@ class MathAggregator(RoutedAgent):
answers = [resp.answer for resp in self._responses[message.session_id]] answers = [resp.answer for resp in self._responses[message.session_id]]
majority_answer = max(set(answers), key=answers.count) majority_answer = max(set(answers), key=answers.count)
# Publish the aggregated response. # Publish the aggregated response.
assert ctx.topic_id is not None await self.publish_message(Answer(content=majority_answer), topic_id=DefaultTopicId())
await self.publish_message(Answer(content=majority_answer), topic_id=ctx.topic_id)
# Clear the responses. # Clear the responses.
self._responses.pop(message.session_id) self._responses.pop(message.session_id)
print(f"Aggregated answer: {majority_answer}") print(f"Aggregated answer: {majority_answer}")

View File

@ -20,7 +20,7 @@ from dataclasses import dataclass
from typing import Dict, List from typing import Dict, List
from agnext.application import SingleThreadedAgentRuntime from agnext.application import SingleThreadedAgentRuntime
from agnext.components import FunctionCall, RoutedAgent, message_handler from agnext.components import DefaultTopicId, FunctionCall, RoutedAgent, message_handler
from agnext.components._type_subscription import TypeSubscription from agnext.components._type_subscription import TypeSubscription
from agnext.components.code_executor import LocalCommandLineCodeExecutor from agnext.components.code_executor import LocalCommandLineCodeExecutor
from agnext.components.models import ( from agnext.components.models import (
@ -90,8 +90,7 @@ class ToolExecutorAgent(RoutedAgent):
session_id=message.session_id, session_id=message.session_id,
result=FunctionExecutionResult(content=result_as_str, call_id=message.function_call.id), result=FunctionExecutionResult(content=result_as_str, call_id=message.function_call.id),
) )
assert ctx.topic_id is not None await self.publish_message(task_result, topic_id=DefaultTopicId())
await self.publish_message(task_result, topic_id=ctx.topic_id)
class ToolUseAgent(RoutedAgent): class ToolUseAgent(RoutedAgent):
@ -129,8 +128,7 @@ class ToolUseAgent(RoutedAgent):
if isinstance(response.content, str): if isinstance(response.content, str):
# If the response is a string, just publish the response. # If the response is a string, just publish the response.
response_message = AgentResponse(content=response.content) response_message = AgentResponse(content=response.content)
assert ctx.topic_id is not None await self.publish_message(response_message, topic_id=DefaultTopicId())
await self.publish_message(response_message, topic_id=ctx.topic_id)
print(f"AI Response: {response.content}") print(f"AI Response: {response.content}")
return return
@ -143,8 +141,7 @@ class ToolUseAgent(RoutedAgent):
for function_call in response.content: for function_call in response.content:
task = ToolExecutionTask(session_id=session_id, function_call=function_call) task = ToolExecutionTask(session_id=session_id, function_call=function_call)
self._tool_counter[session_id] += 1 self._tool_counter[session_id] += 1
assert ctx.topic_id is not None await self.publish_message(task, topic_id=DefaultTopicId())
await self.publish_message(task, topic_id=ctx.topic_id)
@message_handler @message_handler
async def handle_tool_result(self, message: ToolExecutionTaskResult, ctx: MessageContext) -> None: async def handle_tool_result(self, message: ToolExecutionTaskResult, ctx: MessageContext) -> None:
@ -170,11 +167,10 @@ class ToolUseAgent(RoutedAgent):
self._sessions[message.session_id].append( self._sessions[message.session_id].append(
AssistantMessage(content=response.content, source=self.metadata["type"]) AssistantMessage(content=response.content, source=self.metadata["type"])
) )
assert ctx.topic_id is not None
# If the response is a string, just publish the response. # If the response is a string, just publish the response.
if isinstance(response.content, str): if isinstance(response.content, str):
response_message = AgentResponse(content=response.content) response_message = AgentResponse(content=response.content)
await self.publish_message(response_message, topic_id=ctx.topic_id) await self.publish_message(response_message, topic_id=DefaultTopicId())
self._tool_results.pop(message.session_id) self._tool_results.pop(message.session_id)
self._tool_counter.pop(message.session_id) self._tool_counter.pop(message.session_id)
print(f"AI Response: {response.content}") print(f"AI Response: {response.content}")
@ -185,7 +181,7 @@ class ToolUseAgent(RoutedAgent):
for function_call in response.content: for function_call in response.content:
task = ToolExecutionTask(session_id=message.session_id, function_call=function_call) task = ToolExecutionTask(session_id=message.session_id, function_call=function_call)
self._tool_counter[message.session_id] += 1 self._tool_counter[message.session_id] += 1
await self.publish_message(task, topic_id=ctx.topic_id) await self.publish_message(task, topic_id=DefaultTopicId())
async def main() -> None: async def main() -> None:

View File

@ -4,7 +4,7 @@ from dataclasses import dataclass
from typing import Any, NoReturn from typing import Any, NoReturn
from agnext.application import WorkerAgentRuntime from agnext.application import WorkerAgentRuntime
from agnext.components import RoutedAgent, message_handler from agnext.components import DefaultTopicId, RoutedAgent, message_handler
from agnext.components._type_subscription import TypeSubscription from agnext.components._type_subscription import TypeSubscription
from agnext.core import MESSAGE_TYPE_REGISTRY, MessageContext, TopicId from agnext.core import MESSAGE_TYPE_REGISTRY, MessageContext, TopicId
@ -40,13 +40,11 @@ class ReceiveAgent(RoutedAgent):
@message_handler @message_handler
async def on_greet(self, message: Greeting, ctx: MessageContext) -> None: async def on_greet(self, message: Greeting, ctx: MessageContext) -> None:
assert ctx.topic_id is not None await self.publish_message(ReturnedGreeting(f"Returned greeting: {message.content}"), topic_id=DefaultTopicId())
await self.publish_message(ReturnedGreeting(f"Returned greeting: {message.content}"), topic_id=ctx.topic_id)
@message_handler @message_handler
async def on_feedback(self, message: Feedback, ctx: MessageContext) -> None: async def on_feedback(self, message: Feedback, ctx: MessageContext) -> None:
assert ctx.topic_id is not None await self.publish_message(ReturnedFeedback(f"Returned feedback: {message.content}"), topic_id=DefaultTopicId())
await self.publish_message(ReturnedFeedback(f"Returned feedback: {message.content}"), topic_id=ctx.topic_id)
async def on_unhandled_message(self, message: Any, ctx: MessageContext) -> NoReturn: # type: ignore async def on_unhandled_message(self, message: Any, ctx: MessageContext) -> NoReturn: # type: ignore
print(f"Unhandled message: {message}") print(f"Unhandled message: {message}")
@ -58,13 +56,11 @@ class GreeterAgent(RoutedAgent):
@message_handler @message_handler
async def on_ask(self, message: AskToGreet, ctx: MessageContext) -> None: async def on_ask(self, message: AskToGreet, ctx: MessageContext) -> None:
assert ctx.topic_id is not None await self.publish_message(Greeting(f"Hello, {message.content}!"), topic_id=DefaultTopicId())
await self.publish_message(Greeting(f"Hello, {message.content}!"), topic_id=ctx.topic_id)
@message_handler @message_handler
async def on_returned_greet(self, message: ReturnedGreeting, ctx: MessageContext) -> None: async def on_returned_greet(self, message: ReturnedGreeting, ctx: MessageContext) -> None:
assert ctx.topic_id is not None await self.publish_message(Feedback(f"Feedback: {message.content}"), topic_id=DefaultTopicId())
await self.publish_message(Feedback(f"Feedback: {message.content}"), topic_id=ctx.topic_id)
async def on_unhandled_message(self, message: Any, ctx: MessageContext) -> NoReturn: # type: ignore async def on_unhandled_message(self, message: Any, ctx: MessageContext) -> NoReturn: # type: ignore
print(f"Unhandled message: {message}") print(f"Unhandled message: {message}")

View File

@ -4,7 +4,7 @@ from dataclasses import dataclass
from typing import Any, NoReturn from typing import Any, NoReturn
from agnext.application import WorkerAgentRuntime from agnext.application import WorkerAgentRuntime
from agnext.components import RoutedAgent, TypeSubscription, message_handler from agnext.components import DefaultTopicId, RoutedAgent, TypeSubscription, message_handler
from agnext.core import MESSAGE_TYPE_REGISTRY, AgentId, AgentInstantiationContext, MessageContext, TopicId from agnext.core import MESSAGE_TYPE_REGISTRY, AgentId, AgentInstantiationContext, MessageContext, TopicId
@ -47,8 +47,7 @@ class GreeterAgent(RoutedAgent):
@message_handler @message_handler
async def on_ask(self, message: AskToGreet, ctx: MessageContext) -> None: async def on_ask(self, message: AskToGreet, ctx: MessageContext) -> None:
response = await self.send_message(Greeting(f"Hello, {message.content}!"), recipient=self._receive_agent_id) response = await self.send_message(Greeting(f"Hello, {message.content}!"), recipient=self._receive_agent_id)
assert ctx.topic_id is not None await self.publish_message(Feedback(f"Feedback: {response.content}"), topic_id=DefaultTopicId())
await self.publish_message(Feedback(f"Feedback: {response.content}"), topic_id=ctx.topic_id)
async def on_unhandled_message(self, message: Any, ctx: MessageContext) -> NoReturn: # type: ignore async def on_unhandled_message(self, message: Any, ctx: MessageContext) -> NoReturn: # type: ignore
print(f"Unhandled message: {message}") print(f"Unhandled message: {message}")

View File

@ -6,7 +6,7 @@ from ._closure_agent import ClosureAgent
from ._default_subscription import DefaultSubscription from ._default_subscription import DefaultSubscription
from ._default_topic import DefaultTopicId from ._default_topic import DefaultTopicId
from ._image import Image from ._image import Image
from ._routed_agent import RoutedAgent, message_handler, TypeRoutedAgent from ._routed_agent import RoutedAgent, TypeRoutedAgent, message_handler
from ._type_subscription import TypeSubscription from ._type_subscription import TypeSubscription
from ._types import FunctionCall from ._types import FunctionCall

View File

@ -2,6 +2,7 @@ from dataclasses import dataclass
from typing import Any from typing import Any
from agnext.components import RoutedAgent, message_handler from agnext.components import RoutedAgent, message_handler
from agnext.components import DefaultTopicId
from agnext.core import BaseAgent from agnext.core import BaseAgent
from agnext.core import MessageContext from agnext.core import MessageContext
@ -38,8 +39,7 @@ class CascadingAgent(RoutedAgent):
self.num_calls += 1 self.num_calls += 1
if message.round == self.max_rounds: if message.round == self.max_rounds:
return return
assert ctx.topic_id is not None await self.publish_message(CascadingMessageType(round=message.round + 1), topic_id=DefaultTopicId())
await self.publish_message(CascadingMessageType(round=message.round + 1), topic_id=ctx.topic_id)
class NoopAgent(BaseAgent): class NoopAgent(BaseAgent):
def __init__(self) -> None: def __init__(self) -> None: