Repository: microsoft/autogen
Pull request: Implement 'candidate_func' parameter to filter down the pool of candidates for selection
## Summary of Changes
- Added 'candidate_func' to 'SelectorGroupChat' to narrow-down the pool of candidate speakers.
- Introduced a test in tests/test_group_chat_endpoint.py to validate its functionality.
- Updated the selector group chat user guide with an example demonstrating 'candidate_func'.

## Why are these changes needed?
- These changes adds a new parameter `candidate_func` to `SelectorGroupChat` that helps user narrow-down the set of agents for speaker selection, allowing users to automatically select next speaker from a smaller pool of agents.

## Related issue number
Closes #5828

## Checks
- [x] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> 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:
`candidate_func` in `SelectorGroupChat`
### Confirmation

- [x] I confirm that I am a maintainer and so can use this template. If I am not, I understand this issue will be closed and I will be asked to use a different template.

### Issue body

In `SelectorGroupChat`, the `selector_func` allows for customizing an exact selection of agents to speak next. 

In many cases, we still want to rely on the model to make a selection, but with a narrowed-down pool of candidates. So it is helpful to introduce a new parameter `candidate_func` which returns a list of potential candidates for the model to select from. 

This is also to replicate the same `allowed_transitions` graph parameter in v0.2.

Hints from issue discussion:
Hi, can I work on this?
@Ethan0456 sure. Feel free to ask more clarification. 

This `candidate_func` should be applied only if the `selector_func` is not set, as part of the `select_speaker` implementation. 

Also make sure to add unit test and API documentation for it.

Commit message: Remove 'participants' parameter from 'candidate_func' and increase its precedence over 'allowed_repeated_speaker' (#5828)

Signed-off-by: Abhijeetsingh Meena <abhijeet040403@gmail.com>

Patch to review:
diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py
index 0403025dec69..e4b4507da6e7 100644
--- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py
+++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py
@@ -45,6 +45,7 @@ def __init__(
         allow_repeated_speaker: bool,
         selector_func: Callable[[Sequence[AgentEvent | ChatMessage]], str | None] | None,
         max_selector_attempts: int,
+        candidate_func: Callable[[Sequence[AgentEvent | ChatMessage]], List[str]] | None,
     ) -> None:
         super().__init__(
             name,
@@ -63,6 +64,7 @@ def __init__(
         self._allow_repeated_speaker = allow_repeated_speaker
         self._selector_func = selector_func
         self._max_selector_attempts = max_selector_attempts
+        self._candidate_func = candidate_func
 
     async def validate_group_state(self, messages: List[ChatMessage] | None) -> None:
         pass
@@ -106,6 +108,21 @@ async def select_speaker(self, thread: List[AgentEvent | ChatMessage]) -> str:
                     )
                 # Skip the model based selection.
                 return speaker
+        # Use the candidate function to filter participants if provided
+        elif self._candidate_func is not None:
+            participants = self._candidate_func(thread)
+            if not participants:
+                raise ValueError(
+                    "Candidate function returned an empty list of participants."
+                )
+        else:
+            # Construct the candidate agent list to be selected from, skip the previous speaker if not allowed.
+            if self._previous_speaker is not None and not self._allow_repeated_speaker:
+                participants = [p for p in self._participant_names if p != self._previous_speaker]
+            else:
+                participants = list(self._participant_names)
+
+        assert len(participants) > 0
 
         # Construct the history of the conversation.
         history_messages: List[str] = []
@@ -136,13 +153,6 @@ async def select_speaker(self, thread: List[AgentEvent | ChatMessage]) -> str:
             roles += re.sub(r"\s+", " ", f"{topic_type}: {description}").strip() + "\n"
         roles = roles.strip()
 
-        # Construct the candidate agent list to be selected from, skip the previous speaker if not allowed.
-        if self._previous_speaker is not None and not self._allow_repeated_speaker:
-            participants = [p for p in self._participant_names if p != self._previous_speaker]
-        else:
-            participants = list(self._participant_names)
-        assert len(participants) > 0
-
         # Select the next speaker.
         if len(participants) > 1:
             agent_name = await self._select_speaker(roles, participants, history, self._max_selector_attempts)
@@ -277,6 +287,10 @@ class SelectorGroupChat(BaseGroupChat, Component[SelectorGroupChatConfig]):
             function that takes the conversation history and returns the name of the next speaker.
             If provided, this function will be used to override the model to select the next speaker.
             If the function returns None, the model will be used to select the next speaker.
+        candidate_func (Callable[[Sequence[AgentEvent | ChatMessage]], List[str]], optional):
+            A custom function that takes the conversation history and returns a filtered list of candidates for the next speaker
+            selection using model. If the function returns an empty list or `None`, `SelectorGroupChat` will raise a `ValueError`.
+            This function is only valid if `selector_func` is not set. The `allow_repeated_speaker` will be ignored if set.
 
 
     Raises:
@@ -417,6 +431,7 @@ def __init__(
         allow_repeated_speaker: bool = False,
         max_selector_attempts: int = 3,
         selector_func: Callable[[Sequence[AgentEvent | ChatMessage]], str | None] | None = None,
+        candidate_func: Callable[[Sequence[AgentEvent | ChatMessage]], List[str]] | None = None,
     ):
         super().__init__(
             participants,
@@ -434,6 +449,7 @@ def __init__(
         self._allow_repeated_speaker = allow_repeated_speaker
         self._selector_func = selector_func
         self._max_selector_attempts = max_selector_attempts
+        self._candidate_func = candidate_func
 
     def _create_group_chat_manager_factory(
         self,
@@ -462,6 +478,7 @@ def _create_group_chat_manager_factory(
             self._allow_repeated_speaker,
             self._selector_func,
             self._max_selector_attempts,
+            self._candidate_func
         )
 
     def _to_config(self) -> SelectorGroupChatConfig:
diff --git a/python/packages/autogen-agentchat/tests/test_group_chat_endpoint.py b/python/packages/autogen-agentchat/tests/test_group_chat_endpoint.py
index e32d8fd97feb..67424a6957d9 100644
--- a/python/packages/autogen-agentchat/tests/test_group_chat_endpoint.py
+++ b/python/packages/autogen-agentchat/tests/test_group_chat_endpoint.py
@@ -6,6 +6,7 @@
 from autogen_agentchat.ui import Console
 from autogen_core.models import ChatCompletionClient
 from autogen_ext.models.openai import OpenAIChatCompletionClient
+from autogen_agentchat.base import TaskResult
 
 
 async def _test_selector_group_chat(model_client: ChatCompletionClient) -> None:
@@ -27,6 +28,52 @@ async def _test_selector_group_chat(model_client: ChatCompletionClient) -> None:
     await Console(team.run_stream(task="Draft a short email about organizing a holiday party for new year."))
 
 
+async def _test_selector_group_chat_with_candidate_func(model_client: ChatCompletionClient) -> None:
+
+    filtered_participants = ['developer', 'tester']
+
+    def dummy_candidate_func(thread):
+        # Dummy candidate function that will return
+        # only return developer and reviewer
+        return filtered_participants
+
+    developer = AssistantAgent(
+        "developer",
+        description="Writes and implements code based on requirements.",
+        model_client=model_client,
+        system_message="You are a software developer working on a new feature."
+    )
+
+    tester = AssistantAgent(
+        "tester",
+        description="Writes and executes test cases to validate the implementation.",
+        model_client=model_client,
+        system_message="You are a software tester ensuring the feature works correctly."
+    )
+
+    project_manager = AssistantAgent(
+        "project_manager",
+        description="Oversees the project and ensures alignment with the broader goals.",
+        model_client=model_client,
+        system_message="You are a project manager ensuring the team meets the project goals."
+    )
+
+    team = SelectorGroupChat(
+        participants=[developer, tester, project_manager],
+        model_client=model_client,
+        max_turns=3,
+        candidate_func=dummy_candidate_func,
+    )
+
+    task = "Create a detailed implementation plan for adding dark mode in a React app and review it for feasibility and improvements."
+
+    async for message in team.run_stream(task=task):
+        if not isinstance(message, TaskResult):
+            if message.source == 'user':  # ignore the first 'user' message
+                continue
+            assert message.source in filtered_participants, "Candidate function didn't filter the participants"
+
+
 @pytest.mark.asyncio
 async def test_selector_group_chat_gemini() -> None:
     try:
@@ -39,6 +86,7 @@ async def test_selector_group_chat_gemini() -> None:
         api_key=api_key,
     )
     await _test_selector_group_chat(model_client)
+    await _test_selector_group_chat_with_candidate_func(model_client)
 
 
 @pytest.mark.asyncio
@@ -53,3 +101,4 @@ async def test_selector_group_chat_openai() -> None:
         api_key=api_key,
     )
     await _test_selector_group_chat(model_client)
+    await _test_selector_group_chat_with_candidate_func(model_client)
diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/selector-group-chat.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/selector-group-chat.ipynb
index ee3caf5583d2..2b2357f904ec 100644
--- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/selector-group-chat.ipynb
+++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/selector-group-chat.ipynb
@@ -20,6 +20,7 @@
     "- Prevention of consecutive turns by the same speaker (optional)\n",
     "- Customizable selection prompting\n",
     "- Customizable selection function to override the default model-based selection\n",
+    "- Customizable candidate function to narrow-down the set of agents for selection using model\n",
     "\n",
     "```{note}\n",
     "{py:class}`~autogen_agentchat.teams.SelectorGroupChat` is a high-level API. For more control and customization, refer to the [Group Chat Pattern](../core-user-guide/design-patterns/group-chat.ipynb) in the Core API documentation to implement your own group chat logic.\n",
@@ -56,7 +57,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "from typing import Sequence\n",
+    "from typing import Sequence, List\n",
     "\n",
     "from autogen_agentchat.agents import AssistantAgent, UserProxyAgent\n",
     "from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination\n",
@@ -547,6 +548,161 @@
     "```"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Custom Candidate Function"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "One more possible requirement might be to automatically select the next speaker from a filtered list of agents.\n",
+    "For this, we can set `candidate_func` parameter with a custom candidate function to filter down the list of potential agents for speaker selection for each turn of groupchat.\n",
+    "\n",
+    "This allow us to restrict speaker selection to a specific set of agents after a given agent.\n",
+    "\n",
+    "\n",
+    "```{note}\n",
+    "The `candidate_func` is only valid if `selector_func` is not set.\n",
+    "Returning `None` or an empty list `[]` from the custom candidate function will raise a `ValueError`.\n",
+    "```"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "---------- user ----------\n",
+      "Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?\n",
+      "---------- PlanningAgent ----------\n",
+      "To answer this question, we'll break it down into two main subtasks:\n",
+      "\n",
+      "1. Identify the Miami Heat player with the highest points in the 2006-2007 season.\n",
+      "2. Calculate the percentage change in that player's total rebounds between the 2007-2008 and 2008-2009 seasons.\n",
+      "\n",
+      "Let's assign these tasks:\n",
+      "\n",
+      "1. WebSearchAgent: Search for the Miami Heat player with the highest points in the 2006-2007 NBA season.\n",
+      "2. WebSearchAgent: Find the total rebound statistics for that identified player for both the 2007-2008 and 2008-2009 NBA seasons.\n",
+      "3. DataAnalystAgent: Calculate the percentage change in the player's total rebounds between the 2007-2008 and 2008-2009 seasons once the data is retrieved.\n",
+      "---------- WebSearchAgent ----------\n",
+      "[FunctionCall(id='call_WtR5KTfEIxs3jIO25gjAw7dF', arguments='{\"query\":\"Miami Heat highest points scorer 2006-2007 NBA season\"}', name='search_web_tool')]\n",
+      "---------- WebSearchAgent ----------\n",
+      "[FunctionExecutionResult(content='Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n        Udonis Haslem: 844 points\\n        Dwayne Wade: 1397 points\\n        James Posey: 550 points\\n        ...\\n        ', name='search_web_tool', call_id='call_WtR5KTfEIxs3jIO25gjAw7dF', is_error=False)]\n",
+      "---------- WebSearchAgent ----------\n",
+      "Here are the total points scored by Miami Heat players in the 2006-2007 season:\n",
+      "        Udonis Haslem: 844 points\n",
+      "        Dwayne Wade: 1397 points\n",
+      "        James Posey: 550 points\n",
+      "        ...\n",
+      "        \n",
+      "---------- DataAnalystAgent ----------\n",
+      "[FunctionCall(id='call_9HA3DEacUl4WuG2G2PtRkXAO', arguments='{\"start\": 432, \"end\": 527}', name='percentage_change_tool')]\n",
+      "---------- DataAnalystAgent ----------\n",
+      "[FunctionExecutionResult(content='21.99074074074074', name='percentage_change_tool', call_id='call_9HA3DEacUl4WuG2G2PtRkXAO', is_error=False)]\n",
+      "---------- DataAnalystAgent ----------\n",
+      "21.99074074074074\n",
+      "---------- PlanningAgent ----------\n",
+      "It seems we've missed some context there, so let's assign the subtasks again for clarity:\n",
+      "\n",
+      "Based on the search results, Dwyane Wade had the highest points for the Miami Heat in the 2006-2007 season with 1397 points.\n",
+      "\n",
+      "Now, let's find the necessary rebound statistics:\n",
+      "\n",
+      "2. WebSearchAgent: Find Dwyane Wade's total rebound statistics for both the 2007-2008 and 2008-2009 NBA seasons.\n",
+      "3. DataAnalystAgent: Once the data is retrieved, calculate the percentage change in Dwyane Wade's total rebounds between the 2007-2008 and 2008-2009 seasons.\n",
+      "---------- WebSearchAgent ----------\n",
+      "[FunctionCall(id='call_3i1wTDSjkGg6Ev8YKYWkZK55', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 NBA season\"}', name='search_web_tool'), FunctionCall(id='call_NRAs6jHxXRi8zsvpW5WlHAaU', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 NBA season\"}', name='search_web_tool')]\n",
+      "---------- WebSearchAgent ----------\n",
+      "[FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.', name='search_web_tool', call_id='call_3i1wTDSjkGg6Ev8YKYWkZK55', is_error=False), FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', name='search_web_tool', call_id='call_NRAs6jHxXRi8zsvpW5WlHAaU', is_error=False)]\n",
+      "---------- WebSearchAgent ----------\n",
+      "The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.\n",
+      "The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.\n",
+      "---------- PlanningAgent ----------\n",
+      "The total rebounds for Dwyane Wade in the 2007-2008 season were 214, and in the 2008-2009 season, they were 398.\n",
+      "\n",
+      "Now, let's calculate the percentage change.\n",
+      "\n",
+      "3. DataAnalystAgent: Calculate the percentage change in Dwyane Wade's total rebounds from the 2007-2008 season to the 2008-2009 season.\n",
+      "---------- DataAnalystAgent ----------\n",
+      "[FunctionCall(id='call_XECA7ezz7VIKbf8IbZYSCSpI', arguments='{\"start\":214,\"end\":398}', name='percentage_change_tool')]\n",
+      "---------- DataAnalystAgent ----------\n",
+      "[FunctionExecutionResult(content='85.98130841121495', name='percentage_change_tool', call_id='call_XECA7ezz7VIKbf8IbZYSCSpI', is_error=False)]\n",
+      "---------- DataAnalystAgent ----------\n",
+      "85.98130841121495\n",
+      "---------- PlanningAgent ----------\n",
+      "The Miami Heat player with the highest points in the 2006-2007 season was Dwyane Wade, with 1397 points. The percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons was approximately 85.98%.\n",
+      "\n",
+      "TERMINATE\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "TaskResult(messages=[TextMessage(source='user', models_usage=None, metadata={}, content='Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=161, completion_tokens=169), metadata={}, content=\"To answer this question, we'll break it down into two main subtasks:\\n\\n1. Identify the Miami Heat player with the highest points in the 2006-2007 season.\\n2. Calculate the percentage change in that player's total rebounds between the 2007-2008 and 2008-2009 seasons.\\n\\nLet's assign these tasks:\\n\\n1. WebSearchAgent: Search for the Miami Heat player with the highest points in the 2006-2007 NBA season.\\n2. WebSearchAgent: Find the total rebound statistics for that identified player for both the 2007-2008 and 2008-2009 NBA seasons.\\n3. DataAnalystAgent: Calculate the percentage change in the player's total rebounds between the 2007-2008 and 2008-2009 seasons once the data is retrieved.\", type='TextMessage'), ToolCallRequestEvent(source='WebSearchAgent', models_usage=RequestUsage(prompt_tokens=324, completion_tokens=28), metadata={}, content=[FunctionCall(id='call_WtR5KTfEIxs3jIO25gjAw7dF', arguments='{\"query\":\"Miami Heat highest points scorer 2006-2007 NBA season\"}', name='search_web_tool')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='WebSearchAgent', models_usage=None, metadata={}, content=[FunctionExecutionResult(content='Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n        Udonis Haslem: 844 points\\n        Dwayne Wade: 1397 points\\n        James Posey: 550 points\\n        ...\\n        ', name='search_web_tool', call_id='call_WtR5KTfEIxs3jIO25gjAw7dF', is_error=False)], type='ToolCallExecutionEvent'), ToolCallSummaryMessage(source='WebSearchAgent', models_usage=None, metadata={}, content='Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n        Udonis Haslem: 844 points\\n        Dwayne Wade: 1397 points\\n        James Posey: 550 points\\n        ...\\n        ', type='ToolCallSummaryMessage'), ToolCallRequestEvent(source='DataAnalystAgent', models_usage=RequestUsage(prompt_tokens=390, completion_tokens=37), metadata={}, content=[FunctionCall(id='call_9HA3DEacUl4WuG2G2PtRkXAO', arguments='{\"start\": 432, \"end\": 527}', name='percentage_change_tool')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='DataAnalystAgent', models_usage=None, metadata={}, content=[FunctionExecutionResult(content='21.99074074074074', name='percentage_change_tool', call_id='call_9HA3DEacUl4WuG2G2PtRkXAO', is_error=False)], type='ToolCallExecutionEvent'), ToolCallSummaryMessage(source='DataAnalystAgent', models_usage=None, metadata={}, content='21.99074074074074', type='ToolCallSummaryMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=413, completion_tokens=137), metadata={}, content=\"It seems we've missed some context there, so let's assign the subtasks again for clarity:\\n\\nBased on the search results, Dwyane Wade had the highest points for the Miami Heat in the 2006-2007 season with 1397 points.\\n\\nNow, let's find the necessary rebound statistics:\\n\\n2. WebSearchAgent: Find Dwyane Wade's total rebound statistics for both the 2007-2008 and 2008-2009 NBA seasons.\\n3. DataAnalystAgent: Once the data is retrieved, calculate the percentage change in Dwyane Wade's total rebounds between the 2007-2008 and 2008-2009 seasons.\", type='TextMessage'), ToolCallRequestEvent(source='WebSearchAgent', models_usage=RequestUsage(prompt_tokens=576, completion_tokens=73), metadata={}, content=[FunctionCall(id='call_3i1wTDSjkGg6Ev8YKYWkZK55', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 NBA season\"}', name='search_web_tool'), FunctionCall(id='call_NRAs6jHxXRi8zsvpW5WlHAaU', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 NBA season\"}', name='search_web_tool')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='WebSearchAgent', models_usage=None, metadata={}, content=[FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.', name='search_web_tool', call_id='call_3i1wTDSjkGg6Ev8YKYWkZK55', is_error=False), FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', name='search_web_tool', call_id='call_NRAs6jHxXRi8zsvpW5WlHAaU', is_error=False)], type='ToolCallExecutionEvent'), ToolCallSummaryMessage(source='WebSearchAgent', models_usage=None, metadata={}, content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.\\nThe number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', type='ToolCallSummaryMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=612, completion_tokens=84), metadata={}, content=\"The total rebounds for Dwyane Wade in the 2007-2008 season were 214, and in the 2008-2009 season, they were 398.\\n\\nNow, let's calculate the percentage change.\\n\\n3. DataAnalystAgent: Calculate the percentage change in Dwyane Wade's total rebounds from the 2007-2008 season to the 2008-2009 season.\", type='TextMessage'), ToolCallRequestEvent(source='DataAnalystAgent', models_usage=RequestUsage(prompt_tokens=720, completion_tokens=21), metadata={}, content=[FunctionCall(id='call_XECA7ezz7VIKbf8IbZYSCSpI', arguments='{\"start\":214,\"end\":398}', name='percentage_change_tool')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='DataAnalystAgent', models_usage=None, metadata={}, content=[FunctionExecutionResult(content='85.98130841121495', name='percentage_change_tool', call_id='call_XECA7ezz7VIKbf8IbZYSCSpI', is_error=False)], type='ToolCallExecutionEvent'), ToolCallSummaryMessage(source='DataAnalystAgent', models_usage=None, metadata={}, content='85.98130841121495', type='ToolCallSummaryMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=718, completion_tokens=63), metadata={}, content='The Miami Heat player with the highest points in the 2006-2007 season was Dwyane Wade, with 1397 points. The percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons was approximately 85.98%.\\n\\nTERMINATE', type='TextMessage')], stop_reason=\"Text 'TERMINATE' mentioned\")"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "def candidate_func(messages: Sequence[AgentEvent | ChatMessage]) -> str | None:\n",
+    "    # keep planning_agent first one to plan out the tasks\n",
+    "    if messages[-1].source == \"user\":\n",
+    "        return [planning_agent.name]\n",
+    "\n",
+    "    # if previous agent is planning_agent and if it explicitely asks for web_search_agent\n",
+    "    # or data_analyst_agent or both (in-case of re-planning or re-assignment of tasks)\n",
+    "    # then return those specific agents\n",
+    "    last_message = messages[-1]\n",
+    "    if last_message.source == planning_agent.name:\n",
+    "        participants = []\n",
+    "        if web_search_agent.name in last_message.content:\n",
+    "            participants.append(web_search_agent.name)\n",
+    "        if data_analyst_agent.name in last_message.content:\n",
+    "            participants.append(data_analyst_agent.name)\n",
+    "        if participants:\n",
+    "            return participants  # SelectorGroupChat will select from the remaining two agents.\n",
+    "\n",
+    "    # we can assume that the task is finished once the web_search_agent\n",
+    "    # and data_analyst_agent have took their turns, thus we send\n",
+    "    # in planning_agent to terminate the chat\n",
+    "    previous_set_of_agents = set(message.source for message in messages)\n",
+    "    if (web_search_agent.name in previous_set_of_agents\n",
+    "        and data_analyst_agent.name in previous_set_of_agents):\n",
+    "        return [planning_agent.name]\n",
+    "\n",
+    "    # if no-conditions are met then return all the agents\n",
+    "    return [planning_agent.name, web_search_agent.name, data_analyst_agent.name]\n",
+    "\n",
+    "# Reset the previous team and run the chat again with the selector function.\n",
+    "await team.reset()\n",
+    "team = SelectorGroupChat(\n",
+    "    [planning_agent, web_search_agent, data_analyst_agent],\n",
+    "    model_client=model_client,\n",
+    "    termination_condition=termination,\n",
+    "    candidate_func=candidate_func,\n",
+    ")\n",
+    "\n",
+    "await Console(team.run_stream(task=task))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "You can see from the conversation log that the Planning Agent returns to conversation once the Web Search Agent and Data Analyst Agent took their turns and it finds that the task was not finished as expected so it called the WebSearchAgent again to get rebound values and then called DataAnalysetAgent to get the percentage change."
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},


Code review comments:
diff_hunk: @@ -287,11 +287,10 @@ class SelectorGroupChat(BaseGroupChat, Component[SelectorGroupChatConfig]):
             function that takes the conversation history and returns the name of the next speaker.
             If provided, this function will be used to override the model to select the next speaker.
             If the function returns None, the model will be used to select the next speaker.
-        candidate_func (Callable[[Sequence[AgentEvent | ChatMessage], List[str]], List[str]], optional):
-            A custom function that takes the conversation history and a list of candidates,
-            returning a filtered list of candidates for the next speaker selection using model.
-            This function is only valid if `selector_func` is not set.
-            If the function returns an empty list or `None`, `SelectorGroupChat` will raise a `ValueError`.
+        candidate_func (Callable[[Sequence[AgentEvent | ChatMessage]], List[str]], optional):
+            A custom function that takes the conversation history and returns a filtered list of candidates for the next speaker
+            selection using model. If the function returns an empty list or `None`, `SelectorGroupChat` will raise a `ValueError`.
+            This function is only valid if `selector_func` is not set. The `allow_repeated_speaker` will be ignored if set.
path: python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py
text: @user1:
```suggestion
            This function is only used if `selector_func` is not set. The `allow_repeated_speaker` will be ignored if set.
```