Repository: microsoft/autogen
Pull request: Feature: Get Nested Agents in a `GroupChat`
<!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. -->

<!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. -->

## Why are these changes needed?
<!-- Please give a short summary of the change and the problem this solves. -->
Adds the capability to get all the agents in a given a `GroupChatManager`. Improves quality of life for the following use case:

--- file_a
```python
def some_team():
    member1 = AssistantAgent(...)
    member2 = UserProxyAgent(...)

    gc = GroupChat(agents=[member1, member2], ...)
    return GroupChatManager(groupchat=gc, ...)
```

--- file_b
```python
import file_a

team1 = file_a.some_team()

# I need access to the agents in team1
agents = team1.all_agents()
```
[Example use case.](https://gist.github.com/WaelKarkoub/9b5d8324d6abc334efcc4d2b5ba5ae66)

## Related issue number

Closes #1632 

## Checks

- [ ] I've included any doc changes needed for https://microsoft.github.io/autogen/. See https://microsoft.github.io/autogen/docs/Contribute#documentation to build and test documentation locally.
- [x] I've added tests (if relevant) corresponding to the changes introduced in this PR.
- [x] I've made sure all auto checks have passed.


Problem statement:
[Feature Request]: Listing out all agents in a GroupChatManager
### Is your feature request related to a problem? Please describe.

I'm not sure if this is currently possible, but it would be useful to list out all the agents present in a `GroupChatManager`. 

### Describe the solution you'd like

I was thinking of something similar to `agent_by_name` in `GroupChat`
```python
def agent_by_name(self, name: str) -> Optional[Agent]:
        """Returns the agent with a given name."""
        return self.present_agents().get(name)

def present_agents(self) -> Dict[str, Agent]:
        agents = dict()
        for agent in self._groupchat.agents:
            if not isinstance(agent, GroupChatManager):
                agents[agent.name] = agent
            else:
                agents.update(agent.present_agents())
        return agents
```
I haven't tested this implementation but should be close.

### Additional context

This could be useful when you're defining teams in different modules

--- file_a
```python
def some_team():
    member1 = AssistantAgent(...)
    member2 = UserProxyAgent(...)

    gc = GroupChat(agents=[member1, member2], ...)
    return GroupChatManager(groupchat=gc, ...)
```

--- file_b
```python
import file_a

team1 = file_a.some_team()

# I need access to the agents in team1
agents = team1.present_agents()
```

Hints from issue discussion:

Commit message: remove unused group chat manager from test

Patch to review:
diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py
index 01c6ad709add..df0599a70579 100644
--- a/autogen/agentchat/groupchat.py
+++ b/autogen/agentchat/groupchat.py
@@ -178,6 +178,18 @@ def agent_by_name(self, name: str) -> Agent:
         """Returns the agent with a given name."""
         return self.agents[self.agent_names.index(name)]
 
+    def all_agents(self) -> Dict[str, Agent]:
+        """Returns all agents in the group chat manager."""
+        agents = dict()
+        for agent in self.agents:
+            if not isinstance(agent, GroupChatManager):
+                agents[agent.name] = agent
+            else:
+                agents[agent.name] = agent
+                # Recursive call for nested teams
+                agents.update(agent.groupchat.all_agents())
+        return agents
+
     def next_agent(self, agent: Agent, agents: Optional[List[Agent]] = None) -> Agent:
         """Return the next agent in the list."""
         if agents is None:
@@ -464,7 +476,7 @@ def __init__(
             **kwargs,
         )
         # Store groupchat
-        self._groupchat = groupchat
+        self.groupchat = groupchat
 
         # Order of register_reply is important.
         # Allow sync chat if initiated using initiate_chat
@@ -482,15 +494,15 @@ def chat_messages_for_summary(self, agent: Agent) -> List[Dict]:
         """The list of messages in the group chat as a conversation to summarize.
         The agent is ignored.
         """
-        return self._groupchat.messages
+        return self.groupchat.messages
 
     def _prepare_chat(self, recipient: ConversableAgent, clear_history: bool, prepare_recipient: bool = True) -> None:
         super()._prepare_chat(recipient, clear_history, prepare_recipient)
 
         if clear_history:
-            self._groupchat.reset()
+            self.groupchat.reset()
 
-        for agent in self._groupchat.agents:
+        for agent in self.groupchat.agents:
             if (recipient != agent or prepare_recipient) and isinstance(agent, ConversableAgent):
                 agent._prepare_chat(self, clear_history, False)
 
@@ -620,7 +632,7 @@ def _raise_exception_on_async_reply_functions(self) -> None:
         """
         super()._raise_exception_on_async_reply_functions()
 
-        for agent in self._groupchat.agents:
+        for agent in self.groupchat.agents:
             agent._raise_exception_on_async_reply_functions()
 
     def clear_agents_history(self, reply: str, groupchat: GroupChat) -> str:
diff --git a/test/agentchat/test_groupchat.py b/test/agentchat/test_groupchat.py
index 1d3401273fca..888afcb82cd7 100644
--- a/test/agentchat/test_groupchat.py
+++ b/test/agentchat/test_groupchat.py
@@ -1,3 +1,4 @@
+from typing import Any, Dict, Optional
 import pytest
 from unittest import mock
 import builtins
@@ -672,6 +673,92 @@ def test_clear_agents_history():
     ]
 
 
+def test_get_all_agents_in_groupchat():
+    def agent(name: str) -> autogen.ConversableAgent:
+        return autogen.ConversableAgent(
+            name=name,
+            max_consecutive_auto_reply=10,
+            human_input_mode="NEVER",
+            llm_config=False,
+        )
+
+    def team(name: str) -> autogen.ConversableAgent:
+        member1 = agent(f"member1_{name}")
+        member2 = agent(f"member2_{name}")
+
+        gc = autogen.GroupChat(agents=[member1, member2], messages=[])
+
+        return autogen.GroupChatManager(groupchat=gc, name=name, llm_config=False)
+
+    user = agent("user")
+    team1 = team("team1")
+    team2 = team("team2")
+
+    gc = autogen.GroupChat(agents=[user, team1, team2], messages=[])
+
+    agents = gc.all_agents()
+    team1_member1 = agents.get("member1_team1")
+
+    assert len(agents) == 7
+    assert team1_member1 is not None
+    assert team1_member1.name == "member1_team1"
+
+
+def test_nested_teams_chat():
+    """Tests chat capabilities of nested teams"""
+    team1_msg = {"content": "Hello from team 1"}
+    team2_msg = {"content": "Hello from team 2"}
+
+    def agent(name: str, auto_reply: Optional[Dict[str, Any]] = None) -> autogen.ConversableAgent:
+        return autogen.ConversableAgent(
+            name=name,
+            max_consecutive_auto_reply=10,
+            human_input_mode="NEVER",
+            llm_config=False,
+            default_auto_reply=auto_reply,
+        )
+
+    def team(name: str, auto_reply: Optional[Dict[str, Any]] = None) -> autogen.ConversableAgent:
+        member1 = agent(f"member1_{name}", auto_reply=auto_reply)
+        member2 = agent(f"member2_{name}", auto_reply=auto_reply)
+
+        gc = autogen.GroupChat(agents=[member1, member2], messages=[])
+
+        return autogen.GroupChatManager(groupchat=gc, name=name, llm_config=False)
+
+    def chat(groupchat_manager: autogen.GroupChatManager):
+        agents = groupchat_manager.groupchat.all_agents()
+
+        team1_member1 = agents.get("member1_team1")
+        team2_member2 = agents.get("member2_team2")
+
+        assert team1_member1 is not None
+        assert team2_member2 is not None
+
+        team1_member1.send(team1_msg, team2_member2, request_reply=True)
+
+    user = agent("user")
+    team1 = team("team1", auto_reply=team1_msg)
+    team2 = team("team2", auto_reply=team2_msg)
+
+    gc = autogen.GroupChat(agents=[user, team1, team2], messages=[])
+    gc_manager = autogen.GroupChatManager(groupchat=gc, llm_config=False)
+
+    chat(gc_manager)
+
+    agents = gc.all_agents()
+    team1_member1 = agents.get("member1_team1")
+    team2_member2 = agents.get("member2_team2")
+
+    assert team1_member1 and team2_member2
+
+    msg = team1_member1.chat_messages[team2_member2][0]
+    reply = team1_member1.chat_messages[team2_member2][1]
+
+    assert msg["content"] == team1_msg["content"]
+    assert reply["content"] == team2_msg["content"]
+
+
 if __name__ == "__main__":
     # test_func_call_groupchat()
     # test_broadcast()


Code review comments:
diff_hunk: @@ -464,7 +476,7 @@ def __init__(
             **kwargs,
         )
         # Store groupchat
-        self._groupchat = groupchat
+        self.groupchat = groupchat
path: autogen/agentchat/groupchat.py
text: @user1:
why this change?

@author:
For nested agents, the only way to get access the groupchat is through `_groupchat`, but I can't access it since it's a private variable. From my understanding, autogenstudio also needs access to the groupchat, so I thought it might be cleaner to change it to a public variable.

@user1:
The cleanest way would be to define a new property `groupchat`, you don't want this variable to be changed from the outside.

diff_hunk: @@ -178,6 +178,18 @@ def agent_by_name(self, name: str) -> Agent:
         """Returns the agent with a given name."""
         return self.agents[self.agent_names.index(name)]
 
+    def all_agents(self) -> Dict[str, Agent]:
path: autogen/agentchat/groupchat.py
text: @user1:
As far as I see, there is no guarantee that agent names are unique. However, this API assumes they are. In case of collision (two distinct agents having the same name), this function will not return both of them. It would be better to return a list of agents.

@author:
Makes sense. The current implementation of `agent_by_name` doesn't account for duplicates as well, so I added a fix for that, let me know what you think