You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

_group_chat_manager.py 7.4 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import logging
  2. from typing import Any, Callable, List, Mapping
  3. from agnext.components import RoutedAgent, message_handler
  4. from agnext.components.memory import ChatMemory
  5. from agnext.components.models import ChatCompletionClient
  6. from agnext.core import AgentId, AgentProxy, MessageContext
  7. from ..types import (
  8. Message,
  9. MultiModalMessage,
  10. PublishNow,
  11. Reset,
  12. TextMessage,
  13. )
  14. from ._group_chat_utils import select_speaker
  15. logger = logging.getLogger("agnext.events")
  16. class GroupChatManager(RoutedAgent):
  17. """An agent that manages a group chat through event-driven orchestration.
  18. Args:
  19. name (str): The name of the agent.
  20. description (str): The description of the agent.
  21. runtime (AgentRuntime): The runtime to register the agent.
  22. participants (List[AgentId]): The list of participants in the group chat.
  23. memory (ChatMemory[Message]): The memory to store and retrieve messages.
  24. model_client (ChatCompletionClient, optional): The client to use for the model.
  25. If provided, the agent will use the model to select the next speaker.
  26. If not provided, the agent will select the next speaker from the list of participants
  27. according to the order given.
  28. termination_word (str, optional): The word that terminates the group chat. Defaults to "TERMINATE".
  29. transitions (Mapping[AgentId, List[AgentId]], optional): The transitions between agents.
  30. Keys are the agents, and values are the list of agents that can follow the key agent. Defaults to {}.
  31. If provided, the group chat manager will use the transitions to select the next speaker.
  32. If a transition is not provided for an agent, the choices fallback to all participants.
  33. If no model client is provided, a transition must have a single value.
  34. on_message_received (Callable[[TextMessage], None], optional): A custom handler to call when a message is received.
  35. Defaults to None.
  36. """
  37. def __init__(
  38. self,
  39. description: str,
  40. participants: List[AgentId],
  41. memory: ChatMemory[Message],
  42. model_client: ChatCompletionClient | None = None,
  43. termination_word: str = "TERMINATE",
  44. transitions: Mapping[AgentId, List[AgentId]] = {},
  45. on_message_received: Callable[[TextMessage | MultiModalMessage], None] | None = None,
  46. ):
  47. super().__init__(description)
  48. self._memory = memory
  49. self._client = model_client
  50. self._participants = participants
  51. self._participant_proxies = dict((p, AgentProxy(p, self.runtime)) for p in participants)
  52. self._termination_word = termination_word
  53. for key, value in transitions.items():
  54. if not value:
  55. # Make sure no empty transitions are provided.
  56. raise ValueError(f"Empty transition list provided for {key.type}.")
  57. if key not in participants:
  58. # Make sure all keys are in the list of participants.
  59. raise ValueError(f"Transition key {key.type} not found in participants.")
  60. for v in value:
  61. if v not in participants:
  62. # Make sure all values are in the list of participants.
  63. raise ValueError(f"Transition value {v.type} not found in participants.")
  64. if self._client is None:
  65. # Make sure there is only one transition for each key if no model client is provided.
  66. if len(value) > 1:
  67. raise ValueError(f"Multiple transitions provided for {key.type} but no model client is provided.")
  68. self._tranistions = transitions
  69. self._on_message_received = on_message_received
  70. @message_handler()
  71. async def on_reset(self, message: Reset, ctx: MessageContext) -> None:
  72. """Handle a reset message. This method clears the memory."""
  73. await self._memory.clear()
  74. @message_handler()
  75. async def on_new_message(self, message: TextMessage | MultiModalMessage, ctx: MessageContext) -> None:
  76. """Handle a message. This method adds the message to the memory, selects the next speaker,
  77. and sends a message to the selected speaker to publish a response."""
  78. # Call the custom on_message_received handler if provided.
  79. if self._on_message_received is not None:
  80. self._on_message_received(message)
  81. # Check if the message contains the termination word.
  82. if isinstance(message, TextMessage) and self._termination_word in message.content:
  83. # Terminate the group chat by not selecting the next speaker.
  84. return
  85. # Save the message to chat memory.
  86. await self._memory.add_message(message)
  87. # Get the last speaker.
  88. last_speaker_name = message.source
  89. last_speaker_index = next((i for i, p in enumerate(self._participants) if p.type == last_speaker_name), None)
  90. # Get the candidates for the next speaker.
  91. if last_speaker_index is not None:
  92. logger.debug(f"Last speaker: {last_speaker_name}")
  93. last_speaker = self._participants[last_speaker_index]
  94. if self._tranistions.get(last_speaker) is not None:
  95. candidates = [c for c in self._participants if c in self._tranistions[last_speaker]]
  96. else:
  97. candidates = self._participants
  98. else:
  99. candidates = self._participants
  100. logger.debug(f"Group chat manager next speaker candidates: {[c.type for c in candidates]}")
  101. # Select speaker.
  102. if len(candidates) == 0:
  103. speaker = None
  104. elif len(candidates) == 1:
  105. speaker = candidates[0]
  106. else:
  107. # More than one candidate, select the next speaker.
  108. if self._client is None:
  109. # If no model client is provided, candidates must be the list of participants.
  110. assert candidates == self._participants
  111. # If no model client is provided, select the next speaker from the list of participants.
  112. if last_speaker_index is not None:
  113. next_speaker_index = (last_speaker_index + 1) % len(self._participants)
  114. speaker = self._participants[next_speaker_index]
  115. else:
  116. # If no last speaker, select the first speaker.
  117. speaker = candidates[0]
  118. else:
  119. # If a model client is provided, select the speaker based on the transitions and the model.
  120. speaker_index = await select_speaker(
  121. self._memory, self._client, [self._participant_proxies[c] for c in candidates]
  122. )
  123. speaker = candidates[speaker_index]
  124. logger.debug(f"Group chat manager selected speaker: {speaker.type if speaker is not None else None}")
  125. if speaker is not None:
  126. # Send the message to the selected speaker to ask it to publish a response.
  127. await self.send_message(PublishNow(), speaker)
  128. def save_state(self) -> Mapping[str, Any]:
  129. return {
  130. "memory": self._memory.save_state(),
  131. "termination_word": self._termination_word,
  132. }
  133. def load_state(self, state: Mapping[str, Any]) -> None:
  134. self._memory.load_state(state["memory"])
  135. self._termination_word = state["termination_word"]