remove _ from send/pub, add docs, (#91)

* remove _ from send/pub, add docs,

* fixes
This commit is contained in:
Jack Gerrits 2024-06-18 15:51:02 -04:00 committed by GitHub
parent 5b01f69b58
commit 4cebf7257b
19 changed files with 108 additions and 36 deletions

View File

@ -54,5 +54,4 @@ html_theme_options = {
autodoc_default_options = {
"members": True,
"undoc-members": True,
"private-members": True
}

View File

@ -49,7 +49,7 @@ When an agent receives a message the runtime will invoke the agent's message han
### Direct Communication
Direct communication is effectively an RPC call directly to another agent. When sending a direct message to another agent, the receiving agent can respond to the message with another message, or simply return `None`. To send a message to another agent, within a message handler use the {py:meth}`agnext.core.BaseAgent._send_message` method. Awaiting this call will return the response of the invoked agent. If the receiving agent raises an exception, this will be propagated back to the sending agent.
Direct communication is effectively an RPC call directly to another agent. When sending a direct message to another agent, the receiving agent can respond to the message with another message, or simply return `None`. To send a message to another agent, within a message handler use the {py:meth}`agnext.core.BaseAgent.send_message` method. Awaiting this call will return the response of the invoked agent. If the receiving agent raises an exception, this will be propagated back to the sending agent.
To send a message to an agent outside of agent handling a message the message should be sent via the runtime with the {py:meth}`agnext.core.AgentRuntime.send_message` method. This is often how an application might "start" a workflow or conversation.
@ -57,7 +57,7 @@ To send a message to an agent outside of agent handling a message the message sh
As part of the agent's implementation it must advertise the message types that it would like to receive when published ({py:attr}`agnext.core.Agent.subscriptions`). If one of these messages is published, the agent's message handler will be invoked. The key difference between direct and broadcast communication is that broadcast communication is not a request/response pattern. When an agent publishes a message it is one way, it is not expecting a response from any other agent. In fact, they cannot respond to the message.
To publish a message to all agents, use the {py:meth}`agnext.core.BaseAgent._publish_message` method. This call must still be awaited to allow the runtime to deliver the message to all agents, but it will always return `None`. If an agent raises an exception while handling a published message, this will be logged but will not be propagated back to the publishing agent.
To publish a message to all agents, use the {py:meth}`agnext.core.BaseAgent.publish_message` method. This call must still be awaited to allow the runtime to deliver the message to all agents, but it will always return `None`. If an agent raises an exception while handling a published message, this will be logged but will not be propagated back to the publishing agent.
To publish a message to all agents outside of an agent handling a message, the message should be published via the runtime with the {py:meth}`agnext.core.AgentRuntime.publish_message` method.

View File

@ -0,0 +1,18 @@
# Namespace
A namespace is a logical boundary between agents. By default, agents in one
namespace cannot communicate with agents in another namespace.
Namespaces are strings, and the default is `default`.
Two possible use cases of agents are:
- Creating a multi-tenant system where each tenant has its own namespace. For
example, a chat system where each tenant has its own set of agents.
- Security boundaries between agent groups. For example, a chat system where
agents in the `admin` namespace can communicate with agents in the `user`
namespace, but not the other way around.
The {py:class}`agnext.core.AgentId` is used to address an agent, it is the combination of the agent's namespace and its name.
When getting an agent reference ({py:meth}`agnext.core.AgentRuntime.get`) or proxy ({py:meth}`agnext.core.AgentRuntime.get_proxy`) from the runtime the namespace can be specified. Agents have an ID property ({py:attr}`agnext.core.Agent.id`) that returns the agent's id. Additionally, the register method takes a factory that can optionally accept the ID as an argument ({py:meth}`agnext.core.AgentRuntime.register`).

View File

@ -10,3 +10,27 @@ Further readings:
1. {py:class}`agnext.core.AgentRuntime`
2. {py:class}`agnext.application.SingleThreadedAgentRuntime`
## Agent Registration
Agents are registered with the runtime using the
{py:meth}`agnext.core.AgentRuntime.register` method. The process of registration
associates some name, which is the `type` of the agent with a factory function
that is able to create an instance of the agent in a given namespace. The reason
for the factory function is to allow automatic creation of agents when they are
needed, including automatic creation of agents for not yet existing namespaces.
Once an agent is registered, a reference to the agent can be retrieved by
calling {py:meth}`agnext.core.AgentRuntime.get` or
{py:meth}`agnext.core.AgentRuntime.get_proxy`. There is a convenience method
{py:meth}`agnext.core.AgentRuntime.register_and_get` that both registers a type
and gets a reference.
A byproduct of this process of `register` + `get` is that
{py:class}`agnext.core.Agent` interface is a purely implementation contract. All
agents must be communicated with via the runtime. This is a key design decision
that allows the runtime to manage the lifecycle of agents, and to provide a
consistent API for interacting with agents. Therefore, to communicate with
another agent the {py:class}`agnext.core.AgentId` must be used. There is a
convenience class {py:meth}`agnext.core.AgentProxy` that bundles an ID and a
runtime together.

View File

@ -38,7 +38,7 @@ class MyAgent(TypeRoutedAgent):
self, message: TextMessage | MultiModalMessage, cancellation_token: CancellationToken
) -> None:
self._received_count += 1
await self._publish_message(
await self.publish_message(
TextMessage(
content=f"I received a message from {message.source}. Message received #{self._received_count}",
source=self.metadata["name"],

View File

@ -29,6 +29,7 @@ common uses, such as chat completion agents, but also allows for fully custom ag
core-concepts/tools
core-concepts/cancellation
core-concepts/logging
core-concepts/namespace
.. toctree::
:caption: Guides

View File

@ -104,7 +104,7 @@ class UserProxyAgent(TypeRoutedAgent): # type: ignore
return
else:
# Publish user input and exit handler.
await self._publish_message(TextMessage(content=user_input, source=self.metadata["name"]))
await self.publish_message(TextMessage(content=user_input, source=self.metadata["name"]))
return

View File

@ -72,7 +72,7 @@ Use the following JSON format to provide your thought on the latest message and
# Publish the response if needed.
if respond is True or str(respond).lower().strip() == "true":
await self._publish_message(TextMessage(source=self.metadata["name"], content=str(response)))
await self.publish_message(TextMessage(source=self.metadata["name"], content=str(response)))
class ChatRoomUserAgent(TextualUserAgent): # type: ignore

View File

@ -30,7 +30,7 @@ class Outer(TypeRoutedAgent): # type: ignore
@message_handler() # type: ignore
async def on_new_message(self, message: MessageType, cancellation_token: CancellationToken) -> MessageType: # type: ignore
inner_response = self._send_message(message, self._inner)
inner_response = self.send_message(message, self._inner)
inner_message = await inner_response
assert isinstance(inner_message, MessageType)
return MessageType(body=f"Outer: {inner_message.body}", sender=self.metadata["name"])

View File

@ -114,7 +114,7 @@ class ChatCompletionAgent(TypeRoutedAgent):
response = await self._generate_response(message.response_format, cancellation_token)
# Publish the response.
await self._publish_message(response)
await self.publish_message(response)
@message_handler()
async def on_tool_call_message(
@ -190,7 +190,7 @@ class ChatCompletionAgent(TypeRoutedAgent):
and all(isinstance(x, FunctionCall) for x in response.content)
):
# Send a function call message to itself.
response = await self._send_message(
response = await self.send_message(
message=FunctionCallMessage(content=response.content, source=self.metadata["name"]),
recipient=self.id,
cancellation_token=cancellation_token,
@ -236,7 +236,7 @@ class ChatCompletionAgent(TypeRoutedAgent):
approval_request = ToolApprovalRequest(
tool_call=FunctionCall(id=call_id, arguments=json.dumps(args), name=name)
)
approval_response = await self._send_message(
approval_response = await self.send_message(
message=approval_request,
recipient=self._tool_approver,
cancellation_token=cancellation_token,

View File

@ -41,7 +41,7 @@ class ImageGenerationAgent(TypeRoutedAgent):
@message_handler
async def on_publish_now(self, message: PublishNow, cancellation_token: CancellationToken) -> None:
response = await self._generate_response(cancellation_token)
self._publish_message(response)
self.publish_message(response)
async def _generate_response(self, cancellation_token: CancellationToken) -> MultiModalMessage:
messages = await self._memory.get_messages()

View File

@ -79,7 +79,7 @@ class OpenAIAssistantAgent(TypeRoutedAgent):
async def on_publish_now(self, message: PublishNow, cancellation_token: CancellationToken) -> None:
"""Handle a publish now message. This method generates a response and publishes it."""
response = await self._generate_response(message.response_format, cancellation_token)
await self._publish_message(response)
await self.publish_message(response)
async def _generate_response(
self, requested_response_format: ResponseFormat, cancellation_token: CancellationToken

View File

@ -24,7 +24,7 @@ class UserProxyAgent(TypeRoutedAgent):
async def on_publish_now(self, message: PublishNow, cancellation_token: CancellationToken) -> None:
"""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)
await self._publish_message(TextMessage(content=user_input, source=self.metadata["name"]))
await self.publish_message(TextMessage(content=user_input, source=self.metadata["name"]))
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."""

View File

@ -141,7 +141,7 @@ class GroupChatManager(TypeRoutedAgent):
if speaker is not None:
# Send the message to the selected speaker to ask it to publish a response.
await self._send_message(PublishNow(), speaker)
await self.send_message(PublishNow(), speaker)
def save_state(self) -> Mapping[str, Any]:
return {

View File

@ -50,7 +50,7 @@ class OrchestratorChat(TypeRoutedAgent):
while total_turns < self._max_turns:
# Reset all agents.
for agent in [*self._specialists, self._orchestrator]:
await self._send_message(Reset(), agent)
await self.send_message(Reset(), agent)
# Create the task specs.
task_specs = f"""
@ -72,7 +72,7 @@ Some additional points to consider:
# Send the task specs to the orchestrator and specialists.
for agent in [*self._specialists, self._orchestrator]:
await self._send_message(TextMessage(content=task_specs, source=self.metadata["name"]), agent)
await self.send_message(TextMessage(content=task_specs, source=self.metadata["name"]), agent)
# Inner loop.
stalled_turns = 0
@ -126,7 +126,7 @@ Some additional points to consider:
# Update agents.
for agent in [*self._specialists, self._orchestrator]:
_ = await self._send_message(
_ = await self.send_message(
TextMessage(content=subtask, source=self.metadata["name"]),
agent,
)
@ -138,12 +138,12 @@ Some additional points to consider:
raise ValueError(f"Invalid next speaker: {data['next_speaker']['answer']}") from e
# Ask speaker to speak.
speaker_response = await self._send_message(RespondNow(), speaker)
speaker_response = await self.send_message(RespondNow(), speaker)
assert speaker_response is not None
# Update all other agents with the speaker's response.
for agent in [agent for agent in self._specialists if agent != speaker] + [self._orchestrator]:
await self._send_message(
await self.send_message(
TextMessage(
content=speaker_response.content,
source=speaker_response.source,
@ -161,7 +161,7 @@ Some additional points to consider:
async def _prepare_task(self, task: str, sender: str) -> Tuple[str, str, str, str]:
# Reset planner.
await self._send_message(Reset(), self._planner)
await self.send_message(Reset(), self._planner)
# A reusable description of the team.
team = "\n".join(
@ -198,8 +198,8 @@ When answering this survey, keep in mind that "facts" will typically be specific
""".strip()
# Ask the planner to obtain prior knowledge about facts.
await self._send_message(TextMessage(content=closed_book_prompt, source=sender), self._planner)
facts_response = await self._send_message(RespondNow(), self._planner)
await self.send_message(TextMessage(content=closed_book_prompt, source=sender), self._planner)
facts_response = await self.send_message(RespondNow(), self._planner)
facts = str(facts_response.content)
@ -211,8 +211,8 @@ When answering this survey, keep in mind that "facts" will typically be specific
Based on the team composition, and known and unknown facts, please devise a short bullet-point plan for addressing the original request. Remember, there is no requirement to involve all team members -- a team member's particular expertise may not be needed for this task.""".strip()
# Send second messag eto the planner.
await self._send_message(TextMessage(content=plan_prompt, source=sender), self._planner)
plan_response = await self._send_message(RespondNow(), self._planner)
await self.send_message(TextMessage(content=plan_prompt, source=sender), self._planner)
plan_response = await self.send_message(RespondNow(), self._planner)
plan = str(plan_response.content)
return team, names, facts, plan
@ -264,9 +264,9 @@ Please output an answer in pure JSON format according to the following schema. T
request = step_prompt
while True:
# Send a message to the orchestrator.
await self._send_message(TextMessage(content=request, source=sender), self._orchestrator)
await self.send_message(TextMessage(content=request, source=sender), self._orchestrator)
# Request a response.
step_response = await self._send_message(
step_response = await self.send_message(
RespondNow(response_format=ResponseFormat.json_object),
self._orchestrator,
)
@ -327,9 +327,9 @@ Please output an answer in pure JSON format according to the following schema. T
{facts}
""".strip()
# Send a message to the orchestrator.
await self._send_message(TextMessage(content=new_facts_prompt, source=sender), self._orchestrator)
await self.send_message(TextMessage(content=new_facts_prompt, source=sender), self._orchestrator)
# Request a response.
new_facts_response = await self._send_message(RespondNow(), self._orchestrator)
new_facts_response = await self.send_message(RespondNow(), self._orchestrator)
return str(new_facts_response.content)
async def _educated_guess(self, facts: str, sender: str) -> Any:
@ -352,12 +352,12 @@ Please output an answer in pure JSON format according to the following schema. T
request = educated_guess_promt
while True:
# Send a message to the orchestrator.
await self._send_message(
await self.send_message(
TextMessage(content=request, source=sender),
self._orchestrator,
)
# Request a response.
response = await self._send_message(
response = await self.send_message(
RespondNow(response_format=ResponseFormat.json_object),
self._orchestrator,
)
@ -386,7 +386,7 @@ Team membership:
{team}
""".strip()
# Send a message to the orchestrator.
await self._send_message(TextMessage(content=new_plan_prompt, source=sender), self._orchestrator)
await self.send_message(TextMessage(content=new_plan_prompt, source=sender), self._orchestrator)
# Request a response.
new_plan_response = await self._send_message(RespondNow(), self._orchestrator)
new_plan_response = await self.send_message(RespondNow(), self._orchestrator)
return str(new_plan_response.content)

View File

@ -5,6 +5,7 @@ from ._model_client import ModelCapabilities
# Based on: https://platform.openai.com/docs/models/continuous-model-upgrades
# This is a moving target, so correctness is checked by the model value returned by openai against expected values at runtime``
_MODEL_POINTERS = {
"gpt-4o": "gpt-4o-2024-05-13",
"gpt-4-turbo": "gpt-4-turbo-2024-04-09",
"gpt-4-turbo-preview": "gpt-4-0125-preview",
"gpt-4": "gpt-4-0613",
@ -14,6 +15,11 @@ _MODEL_POINTERS = {
}
_MODEL_CAPABILITIES: Dict[str, ModelCapabilities] = {
"gpt-4o-2024-05-13": {
"vision": True,
"function_calling": True,
"json_output": True,
},
"gpt-4-turbo-2024-04-09": {
"vision": True,
"function_calling": True,

View File

@ -60,7 +60,31 @@ class AgentRuntime(Protocol):
agent_factory: Callable[[], T] | Callable[[AgentRuntime, AgentId], T],
*,
valid_namespaces: Sequence[str] | Type[AllNamespaces] = AllNamespaces,
) -> None: ...
) -> None:
"""Register an agent factory with the runtime associated with a specific name. The name must be unique.
Args:
name (str): The name of the type agent this factory creates.
agent_factory (Callable[[], T] | Callable[[AgentRuntime, AgentId], T]): The factory that creates the agent.
valid_namespaces (Sequence[str] | Type[AllNamespaces], optional): Valid namespaces for this type. Defaults to AllNamespaces.
Example:
.. code-block:: python
runtime.register(
"chat_agent",
lambda: ChatCompletionAgent(
description="A generic chat agent.",
system_messages=[SystemMessage("You are a helpful assistant")],
model_client=OpenAI(model="gpt-4o"),
memory=BufferedChatMemory(buffer_size=10),
),
)
"""
...
def get(self, name: str, *, namespace: str = "default") -> AgentId: ...
def get_proxy(self, name: str, *, namespace: str = "default") -> AgentProxy: ...

View File

@ -60,7 +60,7 @@ class BaseAgent(ABC, Agent):
async def on_message(self, message: Any, cancellation_token: CancellationToken) -> Any: ...
# Returns the response of the message
def _send_message(
def send_message(
self,
message: Any,
recipient: AgentId,
@ -82,7 +82,7 @@ class BaseAgent(ABC, Agent):
cancellation_token.link_future(future)
return future
def _publish_message(
def publish_message(
self,
message: Any,
*,

View File

@ -43,7 +43,7 @@ class NestingLongRunningAgent(TypeRoutedAgent):
@message_handler
async def on_new_message(self, message: MessageType, cancellation_token: CancellationToken) -> MessageType:
self.called = True
response = self._send_message(message, self._nested_agent, cancellation_token=cancellation_token)
response = self.send_message(message, self._nested_agent, cancellation_token=cancellation_token)
try:
val = await response
assert isinstance(val, MessageType)