CoACT initialize (#292)

This commit is contained in:
Linxin Song
2025-07-30 19:35:20 -07:00
committed by GitHub
parent 862d704b8c
commit b968155757
228 changed files with 42386 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
#
# SPDX-License-Identifier: Apache-2.0
#

View File

@@ -0,0 +1,132 @@
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
#
# SPDX-License-Identifier: Apache-2.0
from typing import TYPE_CHECKING, Any, Optional, Union
from pydantic import BaseModel
from ....doc_utils import export_module
from ...agent import Agent
from ..speaker_selection_result import SpeakerSelectionResult
from .transition_target import AgentTarget, TransitionTarget
from .transition_utils import __AGENT_WRAPPER_PREFIX__
if TYPE_CHECKING:
from ...conversable_agent import ConversableAgent
from ...groupchat import GroupChat
from ..patterns.pattern import Pattern
__all__ = ["GroupChatConfig", "GroupChatTarget"]
@export_module("autogen.agentchat.group")
class GroupChatConfig(BaseModel):
"""Configuration for a group chat transition target.
Note: If context_variables are not passed in, the outer context variables will be passed in"""
pattern: "Pattern"
messages: Union[list[dict[str, Any]], str]
max_rounds: int = 20
@export_module("autogen.agentchat.group")
class GroupChatTarget(TransitionTarget):
"""Target that represents a group chat."""
group_chat_config: GroupChatConfig
def can_resolve_for_speaker_selection(self) -> bool:
"""Check if the target can resolve for speaker selection. For GroupChatTarget the chat must be encapsulated into an agent."""
return False
def resolve(
self,
groupchat: "GroupChat",
current_agent: "ConversableAgent",
user_agent: Optional["ConversableAgent"],
) -> SpeakerSelectionResult:
"""Resolve to the nested chat configuration."""
raise NotImplementedError(
"GroupChatTarget does not support the resolve method. An agent should be used to encapsulate this nested chat and then the target changed to an AgentTarget."
)
def display_name(self) -> str:
"""Get the display name for the target."""
return "a group chat"
def normalized_name(self) -> str:
"""Get a normalized name for the target that has no spaces, used for function calling."""
return "group_chat"
def __str__(self) -> str:
"""String representation for AgentTarget, can be shown as a function call message."""
return "Transfer to group chat"
def needs_agent_wrapper(self) -> bool:
"""Check if the target needs to be wrapped in an agent. GroupChatTarget must be wrapped in an agent."""
return True
def create_wrapper_agent(self, parent_agent: "ConversableAgent", index: int) -> "ConversableAgent":
"""Create a wrapper agent for the group chat."""
from autogen.agentchat import initiate_group_chat
from ...conversable_agent import ConversableAgent # to avoid circular import
# Create the wrapper agent with a name that identifies it as a wrapped group chat
group_chat_agent = ConversableAgent(
name=f"{__AGENT_WRAPPER_PREFIX__}group_{parent_agent.name}_{index + 1}",
# Copy LLM config from parent agent to ensure it can generate replies if needed
llm_config=parent_agent.llm_config,
)
# Store the config directly on the agent
group_chat_agent._group_chat_config = self.group_chat_config # type: ignore[attr-defined]
# Define the reply function that will run the group chat
def group_chat_reply(
agent: "ConversableAgent",
messages: Optional[list[dict[str, Any]]] = None,
sender: Optional["Agent"] = None,
config: Optional[Any] = None,
) -> tuple[bool, Optional[dict[str, Any]]]:
"""Run the inner group chat and return its results as a reply."""
# Get the configuration stored directly on the agent
group_config = agent._group_chat_config # type: ignore[attr-defined]
# Pull through the second last message from the outer chat (the last message will be the handoff message)
# This may need work to make sure we get the right message(s) from the outer chat
message = (
messages[-2]["content"]
if messages and len(messages) >= 2 and "content" in messages[-2]
else "No message to pass through."
)
try:
# Run the group chat with direct agent references from the config
result, _, _ = initiate_group_chat(
pattern=group_config.pattern,
messages=message,
max_rounds=group_config.max_rounds,
)
# Return the summary from the chat result summary
return True, {"content": result.summary}
except Exception as e:
# Handle any errors during execution
return True, {"content": f"Error running group chat: {str(e)}"}
# Register the reply function with the wrapper agent
group_chat_agent.register_reply(
trigger=[ConversableAgent, None],
reply_func=group_chat_reply,
remove_other_reply_funcs=True, # Use only this reply function
)
# After the group chat completes, transition back to the parent agent
group_chat_agent.handoffs.set_after_work(AgentTarget(parent_agent))
return group_chat_agent

View File

@@ -0,0 +1,151 @@
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
#
# SPDX-License-Identifier: Apache-2.0
from typing import TYPE_CHECKING, Any, Optional, Type, Union
from pydantic import BaseModel, field_validator
from ....doc_utils import export_module
from ..context_str import ContextStr
from ..group_tool_executor import GroupToolExecutor
from ..speaker_selection_result import SpeakerSelectionResult
from .transition_target import TransitionTarget
from .transition_utils import __AGENT_WRAPPER_PREFIX__
if TYPE_CHECKING:
# Avoid circular import
from ...conversable_agent import ConversableAgent
from ...groupchat import GroupChat
__all__ = ["GroupManagerTarget"]
def prepare_groupchat_auto_speaker(
groupchat: "GroupChat",
last_group_agent: "ConversableAgent",
group_chat_manager_selection_msg: Optional[Any],
) -> None:
"""Prepare the group chat for auto speaker selection, includes updating or restore the groupchat speaker selection message.
Tool Executor and wrapped agents will be removed from the available agents list.
Args:
groupchat (GroupChat): GroupChat instance.
last_group_agent ("ConversableAgent"): The last group agent for which the LLM config is used
group_chat_manager_selection_msg (GroupManagerSelectionMessage): Optional message to use for the agent selection (in internal group chat).
"""
from ...groupchat import SELECT_SPEAKER_PROMPT_TEMPLATE
def substitute_agentlist(template: str) -> str:
# Run through group chat's string substitution first for {agentlist}
# We need to do this so that the next substitution doesn't fail with agentlist
# and we can remove the tool executor and wrapped chats from the available agents list
agent_list = [
agent
for agent in groupchat.agents
if not isinstance(agent, GroupToolExecutor) and not agent.name.startswith(__AGENT_WRAPPER_PREFIX__)
]
groupchat.select_speaker_prompt_template = template
return groupchat.select_speaker_prompt(agent_list)
# Use the default speaker selection prompt if one is not specified, otherwise use the specified one
groupchat.select_speaker_prompt_template = substitute_agentlist(
SELECT_SPEAKER_PROMPT_TEMPLATE
if group_chat_manager_selection_msg is None
else group_chat_manager_selection_msg.get_message(last_group_agent)
)
# GroupManagerSelectionMessage protocol and implementations
@export_module("autogen.agentchat.group")
class GroupManagerSelectionMessage(BaseModel):
"""Base class for all GroupManager selection message types."""
def get_message(self, agent: "ConversableAgent") -> str:
"""Get the formatted message."""
raise NotImplementedError("Requires subclasses to implement.")
@export_module("autogen.agentchat.group")
class GroupManagerSelectionMessageString(GroupManagerSelectionMessage):
"""Selection message that uses a plain string template."""
message: str
def get_message(self, agent: "ConversableAgent") -> str:
"""Get the message string."""
return self.message
@export_module("autogen.agentchat.group")
class GroupManagerSelectionMessageContextStr(GroupManagerSelectionMessage):
"""Selection message that uses a ContextStr template."""
context_str_template: str
# We will replace {agentlist} with another term and return it later for use with the internal group chat auto speaker selection
# Otherwise our format will fail
@field_validator("context_str_template", mode="before")
def _replace_agentlist_placeholder(cls: Type["GroupManagerSelectionMessageContextStr"], v: Any) -> Union[str, Any]: # noqa: N805
"""Replace {agentlist} placeholder before validation/assignment."""
if isinstance(v, str):
if "{agentlist}" in v:
return v.replace("{agentlist}", "<<agent_list>>") # Perform the replacement
else:
return v # If no replacement is needed, return the original value
return ""
def get_message(self, agent: "ConversableAgent") -> str:
"""Get the formatted message with context variables substituted."""
context_str = ContextStr(template=self.context_str_template)
format_result = context_str.format(agent.context_variables)
if format_result is None:
return ""
return format_result.replace(
"<<agent_list>>", "{agentlist}"
) # Restore agentlist so it can be substituted by the internal group chat auto speaker selection
class GroupManagerTarget(TransitionTarget):
"""Target that represents an agent by name."""
selection_message: Optional[GroupManagerSelectionMessage] = None
def can_resolve_for_speaker_selection(self) -> bool:
"""Check if the target can resolve for speaker selection."""
return True
def resolve(
self,
groupchat: "GroupChat",
current_agent: "ConversableAgent",
user_agent: Optional["ConversableAgent"],
) -> SpeakerSelectionResult:
"""Resolve to the speaker selection for the group."""
if self.selection_message is not None:
prepare_groupchat_auto_speaker(groupchat, current_agent, self.selection_message)
return SpeakerSelectionResult(speaker_selection_method="auto")
def display_name(self) -> str:
"""Get the display name for the target."""
return "the group manager"
def normalized_name(self) -> str:
"""Get a normalized name for the target that has no spaces, used for function calling"""
return self.display_name()
def __str__(self) -> str:
"""String representation for AgentTarget, can be shown as a function call message."""
return "Transfer to the group manager"
def needs_agent_wrapper(self) -> bool:
"""Check if the target needs to be wrapped in an agent."""
return False
def create_wrapper_agent(self, parent_agent: "ConversableAgent", index: int) -> "ConversableAgent":
"""Create a wrapper agent for the target if needed."""
raise NotImplementedError("GroupManagerTarget does not require wrapping in an agent.")

View File

@@ -0,0 +1,413 @@
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
#
# SPDX-License-Identifier: Apache-2.0
import random
from typing import TYPE_CHECKING, Any, Optional
from pydantic import BaseModel
from ..speaker_selection_result import SpeakerSelectionResult
from .transition_utils import __AGENT_WRAPPER_PREFIX__
if TYPE_CHECKING:
# Avoid circular import
from ...conversable_agent import ConversableAgent
from ...groupchat import GroupChat
__all__ = [
"AgentNameTarget",
"AgentTarget",
"AskUserTarget",
"NestedChatTarget",
"RandomAgentTarget",
"RevertToUserTarget",
"StayTarget",
"TerminateTarget",
"TransitionTarget",
]
# Common options for transitions
# terminate: Terminate the conversation
# revert_to_user: Revert to the user agent
# stay: Stay with the current agent
# group_manager: Use the group manager (auto speaker selection)
# ask_user: Use the user manager (ask the user, aka manual)
# TransitionOption = Literal["terminate", "revert_to_user", "stay", "group_manager", "ask_user"]
class TransitionTarget(BaseModel):
"""Base class for all transition targets across OnCondition, OnContextCondition, and after work."""
def can_resolve_for_speaker_selection(self) -> bool:
"""Check if the target can resolve to an option for speaker selection (Agent, 'None' to end, Str for speaker selection method). In the case of a nested chat, this will return False as it should be encapsulated in an agent."""
return False
def resolve(
self,
groupchat: "GroupChat",
current_agent: "ConversableAgent",
user_agent: Optional["ConversableAgent"],
) -> SpeakerSelectionResult:
"""Resolve to a speaker selection result (Agent, None for termination, or str for speaker selection method)."""
raise NotImplementedError("Requires subclasses to implement.")
def display_name(self) -> str:
"""Get the display name for the target."""
raise NotImplementedError("Requires subclasses to implement.")
def normalized_name(self) -> str:
"""Get a normalized name for the target that has no spaces, used for function calling"""
raise NotImplementedError("Requires subclasses to implement.")
def needs_agent_wrapper(self) -> bool:
"""Check if the target needs to be wrapped in an agent."""
raise NotImplementedError("Requires subclasses to implement.")
def create_wrapper_agent(self, parent_agent: "ConversableAgent", index: int) -> "ConversableAgent":
"""Create a wrapper agent for the target if needed."""
raise NotImplementedError("Requires subclasses to implement.")
class AgentTarget(TransitionTarget):
"""Target that represents a direct agent reference."""
agent_name: str
def __init__(self, agent: "ConversableAgent", **data: Any) -> None: # type: ignore[no-untyped-def]
# Store the name from the agent for serialization
super().__init__(agent_name=agent.name, **data)
def can_resolve_for_speaker_selection(self) -> bool:
"""Check if the target can resolve for speaker selection."""
return True
def resolve(
self,
groupchat: "GroupChat",
current_agent: "ConversableAgent",
user_agent: Optional["ConversableAgent"],
) -> SpeakerSelectionResult:
"""Resolve to the actual agent object from the groupchat."""
return SpeakerSelectionResult(agent_name=self.agent_name)
def display_name(self) -> str:
"""Get the display name for the target."""
return f"{self.agent_name}"
def normalized_name(self) -> str:
"""Get a normalized name for the target that has no spaces, used for function calling"""
return self.display_name()
def __str__(self) -> str:
"""String representation for AgentTarget, can be shown as a function call message."""
return f"Transfer to {self.agent_name}"
def needs_agent_wrapper(self) -> bool:
"""Check if the target needs to be wrapped in an agent."""
return False
def create_wrapper_agent(self, parent_agent: "ConversableAgent", index: int) -> "ConversableAgent":
"""Create a wrapper agent for the target if needed."""
raise NotImplementedError("AgentTarget does not require wrapping in an agent.")
class AgentNameTarget(TransitionTarget):
"""Target that represents an agent by name."""
agent_name: str
def __init__(self, agent_name: str, **data: Any) -> None:
"""Initialize with agent name as a positional parameter."""
super().__init__(agent_name=agent_name, **data)
def can_resolve_for_speaker_selection(self) -> bool:
"""Check if the target can resolve for speaker selection."""
return True
def resolve(
self,
groupchat: "GroupChat",
current_agent: "ConversableAgent",
user_agent: Optional["ConversableAgent"],
) -> SpeakerSelectionResult:
"""Resolve to the agent name string."""
return SpeakerSelectionResult(agent_name=self.agent_name)
def display_name(self) -> str:
"""Get the display name for the target."""
return f"{self.agent_name}"
def normalized_name(self) -> str:
"""Get a normalized name for the target that has no spaces, used for function calling"""
return self.display_name()
def __str__(self) -> str:
"""String representation for AgentTarget, can be shown as a function call message."""
return f"Transfer to {self.agent_name}"
def needs_agent_wrapper(self) -> bool:
"""Check if the target needs to be wrapped in an agent."""
return False
def create_wrapper_agent(self, parent_agent: "ConversableAgent", index: int) -> "ConversableAgent":
"""Create a wrapper agent for the target if needed."""
raise NotImplementedError("AgentNameTarget does not require wrapping in an agent.")
class NestedChatTarget(TransitionTarget):
"""Target that represents a nested chat configuration."""
nested_chat_config: dict[str, Any]
def can_resolve_for_speaker_selection(self) -> bool:
"""Check if the target can resolve for speaker selection. For NestedChatTarget the nested chat must be encapsulated into an agent."""
return False
def resolve(
self,
groupchat: "GroupChat",
current_agent: "ConversableAgent",
user_agent: Optional["ConversableAgent"],
) -> SpeakerSelectionResult:
"""Resolve to the nested chat configuration."""
raise NotImplementedError(
"NestedChatTarget does not support the resolve method. An agent should be used to encapsulate this nested chat and then the target changed to an AgentTarget."
)
def display_name(self) -> str:
"""Get the display name for the target."""
return "a nested chat"
def normalized_name(self) -> str:
"""Get a normalized name for the target that has no spaces, used for function calling"""
return "nested_chat"
def __str__(self) -> str:
"""String representation for AgentTarget, can be shown as a function call message."""
return "Transfer to nested chat"
def needs_agent_wrapper(self) -> bool:
"""Check if the target needs to be wrapped in an agent. NestedChatTarget must be wrapped in an agent."""
return True
def create_wrapper_agent(self, parent_agent: "ConversableAgent", index: int) -> "ConversableAgent":
"""Create a wrapper agent for the nested chat."""
from ...conversable_agent import ConversableAgent # to avoid circular import - NEED SOLUTION
nested_chat_agent = ConversableAgent(name=f"{__AGENT_WRAPPER_PREFIX__}nested_{parent_agent.name}_{index + 1}")
nested_chat_agent.register_nested_chats(
self.nested_chat_config["chat_queue"],
reply_func_from_nested_chats=self.nested_chat_config.get("reply_func_from_nested_chats")
or "summary_from_nested_chats",
config=self.nested_chat_config.get("config"),
trigger=lambda sender: True,
position=0,
use_async=self.nested_chat_config.get("use_async", False),
)
# After the nested chat is complete, transfer back to the parent agent
nested_chat_agent.handoffs.set_after_work(AgentTarget(parent_agent))
return nested_chat_agent
class TerminateTarget(TransitionTarget):
"""Target that represents a termination of the conversation."""
def can_resolve_for_speaker_selection(self) -> bool:
"""Check if the target can resolve for speaker selection."""
return True
def resolve(
self,
groupchat: "GroupChat",
current_agent: "ConversableAgent",
user_agent: Optional["ConversableAgent"],
) -> SpeakerSelectionResult:
"""Resolve to termination."""
return SpeakerSelectionResult(terminate=True)
def display_name(self) -> str:
"""Get the display name for the target."""
return "Terminate"
def normalized_name(self) -> str:
"""Get a normalized name for the target that has no spaces, used for function calling"""
return "terminate"
def __str__(self) -> str:
"""String representation for AgentTarget, can be shown as a function call message."""
return "Terminate"
def needs_agent_wrapper(self) -> bool:
"""Check if the target needs to be wrapped in an agent."""
return False
def create_wrapper_agent(self, parent_agent: "ConversableAgent", index: int) -> "ConversableAgent":
"""Create a wrapper agent for the target if needed."""
raise NotImplementedError("TerminateTarget does not require wrapping in an agent.")
class StayTarget(TransitionTarget):
"""Target that represents staying with the current agent."""
def can_resolve_for_speaker_selection(self) -> bool:
"""Check if the target can resolve for speaker selection."""
return True
def resolve(
self,
groupchat: "GroupChat",
current_agent: "ConversableAgent",
user_agent: Optional["ConversableAgent"],
) -> SpeakerSelectionResult:
"""Resolve to staying with the current agent."""
return SpeakerSelectionResult(agent_name=current_agent.name)
def display_name(self) -> str:
"""Get the display name for the target."""
return "Stay"
def normalized_name(self) -> str:
"""Get a normalized name for the target that has no spaces, used for function calling"""
return "stay"
def __str__(self) -> str:
"""String representation for AgentTarget, can be shown as a function call message."""
return "Stay with agent"
def needs_agent_wrapper(self) -> bool:
"""Check if the target needs to be wrapped in an agent."""
return False
def create_wrapper_agent(self, parent_agent: "ConversableAgent", index: int) -> "ConversableAgent":
"""Create a wrapper agent for the target if needed."""
raise NotImplementedError("StayTarget does not require wrapping in an agent.")
class RevertToUserTarget(TransitionTarget):
"""Target that represents reverting to the user agent."""
def can_resolve_for_speaker_selection(self) -> bool:
"""Check if the target can resolve for speaker selection."""
return True
def resolve(
self,
groupchat: "GroupChat",
current_agent: "ConversableAgent",
user_agent: Optional["ConversableAgent"],
) -> SpeakerSelectionResult:
"""Resolve to reverting to the user agent."""
if user_agent is None:
raise ValueError("User agent must be provided to the chat for the revert_to_user option.")
return SpeakerSelectionResult(agent_name=user_agent.name)
def display_name(self) -> str:
"""Get the display name for the target."""
return "Revert to User"
def normalized_name(self) -> str:
"""Get a normalized name for the target that has no spaces, used for function calling"""
return "revert_to_user"
def __str__(self) -> str:
"""String representation for AgentTarget, can be shown as a function call message."""
return "Revert to User"
def needs_agent_wrapper(self) -> bool:
"""Check if the target needs to be wrapped in an agent."""
return False
def create_wrapper_agent(self, parent_agent: "ConversableAgent", index: int) -> "ConversableAgent":
"""Create a wrapper agent for the target if needed."""
raise NotImplementedError("RevertToUserTarget does not require wrapping in an agent.")
class AskUserTarget(TransitionTarget):
"""Target that represents asking the user for input."""
def can_resolve_for_speaker_selection(self) -> bool:
"""Check if the target can resolve for speaker selection."""
return True
def resolve(
self,
groupchat: "GroupChat",
current_agent: "ConversableAgent",
user_agent: Optional["ConversableAgent"],
) -> SpeakerSelectionResult:
"""Resolve to asking the user for input."""
return SpeakerSelectionResult(speaker_selection_method="manual")
def display_name(self) -> str:
"""Get the display name for the target."""
return "Ask User"
def normalized_name(self) -> str:
"""Get a normalized name for the target that has no spaces, used for function calling"""
return "ask_user"
def __str__(self) -> str:
"""String representation for AgentTarget, can be shown as a function call message."""
return "Ask User"
def needs_agent_wrapper(self) -> bool:
"""Check if the target needs to be wrapped in an agent."""
return False
def create_wrapper_agent(self, parent_agent: "ConversableAgent", index: int) -> "ConversableAgent":
"""Create a wrapper agent for the target if needed."""
raise NotImplementedError("AskUserTarget does not require wrapping in an agent.")
class RandomAgentTarget(TransitionTarget):
"""Target that represents a random selection from a list of agents."""
agent_names: list[str]
nominated_name: str = "<Not Randomly Selected Yet>"
def __init__(self, agents: list["ConversableAgent"], **data: Any) -> None: # type: ignore[no-untyped-def]
# Store the name from the agent for serialization
super().__init__(agent_names=[agent.name for agent in agents], **data)
def can_resolve_for_speaker_selection(self) -> bool:
"""Check if the target can resolve for speaker selection."""
return True
def resolve(
self,
groupchat: "GroupChat",
current_agent: "ConversableAgent",
user_agent: Optional["ConversableAgent"],
) -> SpeakerSelectionResult:
"""Resolve to the actual agent object from the groupchat, choosing a random agent (except the current one)"""
# Randomly select the next agent
self.nominated_name = random.choice([name for name in self.agent_names if name != current_agent.name])
return SpeakerSelectionResult(agent_name=self.nominated_name)
def display_name(self) -> str:
"""Get the display name for the target."""
return self.nominated_name
def normalized_name(self) -> str:
"""Get a normalized name for the target that has no spaces, used for function calling"""
return self.display_name()
def __str__(self) -> str:
"""String representation for RandomAgentTarget, can be shown as a function call message."""
return f"Transfer to {self.nominated_name}"
def needs_agent_wrapper(self) -> bool:
"""Check if the target needs to be wrapped in an agent."""
return False
def create_wrapper_agent(self, parent_agent: "ConversableAgent", index: int) -> "ConversableAgent":
"""Create a wrapper agent for the target if needed."""
raise NotImplementedError("RandomAgentTarget does not require wrapping in an agent.")
# TODO: Consider adding a SequentialChatTarget class

View File

@@ -0,0 +1,6 @@
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
#
# SPDX-License-Identifier: Apache-2.0
# Prefix for all wrapped agent names
__AGENT_WRAPPER_PREFIX__ = "wrapped_"