Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graph group chat #857

Merged
merged 92 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 74 commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
721e382
Move contrib-openai.yml
joshkyh Dec 4, 2023
d010679
Moved groupgroupchat
joshkyh Dec 4, 2023
747d0f0
From #753
joshkyh Dec 4, 2023
2dc2aeb
Removed local test references
joshkyh Dec 4, 2023
326bf33
Added ignore=test/agentchat/contrib
joshkyh Dec 4, 2023
07536c7
Trying to pass contrib-openai tests
joshkyh Dec 4, 2023
a2e5a9b
Merge branch 'main' into GraphGroupChat
thinkall Dec 4, 2023
39a2aeb
Merge branch 'main' into GraphGroupChat
joshkyh Dec 5, 2023
ce7bb75
More specific in unit testing.
joshkyh Dec 5, 2023
34e515f
Update .github/workflows/contrib-tests.yml
joshkyh Dec 7, 2023
949d05b
Merge branch 'main' into GraphGroupChat
joshkyh Dec 7, 2023
b78d3b8
Remove coverage as it is included in test dependencies
joshkyh Dec 7, 2023
815110c
Improved docstring with overview of GraphGroupChat
joshkyh Dec 8, 2023
df659fe
Merge branch 'main' into GraphGroupChat
joshkyh Dec 11, 2023
5ad3370
Iterate on feedback
joshkyh Dec 12, 2023
cb687dd
Precommit pass
joshkyh Dec 12, 2023
ad0e703
user just use pip install pyautogen[graphs]
joshkyh Dec 13, 2023
2bf1e21
Merge branch 'main' into GraphGroupChat
BeibinLi Dec 13, 2023
bbb3530
Merge branch 'main' into GraphGroupChat
joshkyh Jan 9, 2024
9cdef51
Pass precommit
joshkyh Jan 9, 2024
444d2ca
Pas precommit
joshkyh Jan 9, 2024
4ea4767
Graph utils an test completed
joshkyh Jan 9, 2024
c0e37e2
Added inversion tests
joshkyh Jan 9, 2024
36bf3eb
Added inversion util
joshkyh Jan 9, 2024
24fbbaa
allow_repeat_speaker can be a list of Agents
joshkyh Jan 9, 2024
9a20c4f
Remove unnessary imports
joshkyh Jan 9, 2024
d4e1e95
Expect ValueError with 1 and 0 agents
joshkyh Jan 9, 2024
cfe9120
Check that main passes all tests
joshkyh Jan 10, 2024
d735e86
Merge branch 'main' into graphgroupchat
joshkyh Jan 10, 2024
317c816
Check main
joshkyh Jan 10, 2024
95b6526
Pytest all in main
joshkyh Jan 10, 2024
59225e0
All done
joshkyh Jan 10, 2024
941b83e
pre-commit changes
joshkyh Jan 10, 2024
a894f84
noqa E402
joshkyh Jan 10, 2024
7314dce
precommit pass
joshkyh Jan 10, 2024
f8d1cca
Removed bin
joshkyh Jan 10, 2024
24a33d6
Removed old unit test
joshkyh Jan 10, 2024
bdd2261
Test test_graph_utils
joshkyh Jan 10, 2024
4e25a76
minor cleanup
joshkyh Jan 10, 2024
04f3583
restore tests
joshkyh Jan 10, 2024
296f9d0
Correct documentation
joshkyh Jan 10, 2024
a81a41e
Special case of only one agent remaining.
joshkyh Jan 10, 2024
3123fa8
Merge branch 'main' into GraphGroupChat
joshkyh Jan 10, 2024
d543848
Improved pytest
joshkyh Jan 10, 2024
c5e35af
precommit pass
joshkyh Jan 10, 2024
f32dc5d
Delete OAI_CONFIG_LIST_sample copy
joshkyh Jan 10, 2024
b8a8b01
Returns a filtered list for auto to work
joshkyh Jan 10, 2024
f813a34
Merge branch 'GraphGroupChat' of https://github.com/microsoft/autogen…
joshkyh Jan 10, 2024
df5c2a0
Merge branch 'main' into GraphGroupChat
joshkyh Jan 11, 2024
7545063
Rename var speaker_order_dict
joshkyh Jan 11, 2024
bae9b14
To write test cases
joshkyh Jan 11, 2024
756ff63
Added check for a list of Agents to repeat
joshkyh Jan 11, 2024
e355183
precommit pass
joshkyh Jan 11, 2024
835204d
Update documentation
joshkyh Jan 11, 2024
c895d4e
Extract names in allow_repeat_speaker
joshkyh Jan 11, 2024
4c88f55
Post review changes
joshkyh Jan 15, 2024
200c39a
hange "pull_request_target" into "pull_request" temporarily.
joshkyh Jan 15, 2024
e958ce6
3 return values from main
joshkyh Jan 15, 2024
fb5953a
pre-commit changes
joshkyh Jan 15, 2024
093fc5f
PC edits
joshkyh Jan 15, 2024
d34e9e5
docstr changes
joshkyh Jan 15, 2024
b35b047
Merge branch 'main' into GraphGroupChat
joshkyh Jan 15, 2024
ba6d7fa
PC edits
joshkyh Jan 15, 2024
eb46013
Rest of changes from main
joshkyh Jan 15, 2024
c9e9a10
Update autogen/agentchat/groupchat.py
joshkyh Jan 17, 2024
dc09c04
Merge branch 'main' into GraphGroupChat
joshkyh Jan 17, 2024
9fcdd88
Remove unnecessary script files from tracking
joshkyh Jan 17, 2024
1c5e4e0
Non empty scripts files from main
joshkyh Jan 17, 2024
be82c53
Revert changes in script files to match main branch
joshkyh Jan 17, 2024
4923bed
Removed link from website as notebook is removed.
joshkyh Jan 17, 2024
d7f848f
test/test_graph_utils.py is tested as part of L52 of build.yml
joshkyh Jan 17, 2024
e89bbc8
GroupChat ValueError check
joshkyh Jan 17, 2024
9b79bad
docstr update
joshkyh Jan 17, 2024
993fd00
More clarification in docstr
joshkyh Jan 17, 2024
1adb3d7
Update autogen/agentchat/groupchat.py
joshkyh Jan 28, 2024
26cdc04
Update autogen/agentchat/groupchat.py
joshkyh Jan 28, 2024
35f9184
Update autogen/agentchat/groupchat.py
joshkyh Jan 28, 2024
6da4e0a
Update autogen/agentchat/groupchat.py
joshkyh Jan 28, 2024
cd826af
Merge remote-tracking branch 'origin/main' into GraphGroupChat
qingyun-wu Feb 1, 2024
31325f9
1.add commit to line138 in groupchat.py;2.fix bug if random choice []…
Feb 3, 2024
c59bf67
Merge branch 'main' into GraphGroupChat
sonichi Feb 4, 2024
6c7c054
fix graph_modelling notebook in the last cell
freedeaths Feb 4, 2024
c91e4a0
fix failure in test_groupchat.py
freedeaths Feb 4, 2024
327a2a1
fix agent out of group to initiate a chat like SocietyOfMind
freedeaths Feb 4, 2024
a93fa44
add a warning rule in graph_utils to check duplicates in any lists
freedeaths Feb 5, 2024
440e99b
refactor allowed_or_disallowed_speaker_transitions to Dict[Agent, Lis…
freedeaths Feb 5, 2024
896d549
delete Rule 4 in graph_utils and related test case. Add a test to res…
freedeaths Feb 5, 2024
cbc7521
Merge branch 'main' into GraphGroupChat
sonichi Feb 5, 2024
3597d1e
fix as the final comments
freedeaths Feb 6, 2024
cc8330c
modify setup option from graphs to graph and add texts in optional-de…
freedeaths Feb 6, 2024
b893cd3
Update autogen/graph_utils.py
sonichi Feb 6, 2024
1e49883
Merge branch 'main' into GraphGroupChat
sonichi Feb 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/contrib-openai.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
name: OpenAI4ContribTests

on:
pull_request_target:
pull_request:
sonichi marked this conversation as resolved.
Show resolved Hide resolved
branches: ['main']
paths:
- 'autogen/**'
Expand Down
136 changes: 125 additions & 11 deletions autogen/agentchat/groupchat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,27 @@
import random
import re
import sys
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Union, Tuple


from ..code_utils import content_str
from .agent import Agent
from .conversable_agent import ConversableAgent
from ..graph_utils import check_graph_validity, invert_disallowed_to_allowed, has_self_loops


logger = logging.getLogger(__name__)


class NoEligibleSpeakerException(Exception):
"""Exception raised for early termination of a GroupChat."""

def __init__(self, message="No eligible speakers."):
self.message = message
super().__init__(self.message)


@dataclass
class GroupChat:
"""(In preview) A group chat class that contains the following data fields:
Expand All @@ -30,7 +41,9 @@ class GroupChat:
- "manual": the next speaker is selected manually by user input.
- "random": the next speaker is selected randomly.
- "round_robin": the next speaker is selected in a round robin fashion, i.e., iterating in the same order as provided in `agents`.
- allow_repeat_speaker: whether to allow the same speaker to speak consecutively. Default is True, in which case all speakers are allowed to speak consecutively. If allow_repeat_speaker is a list of Agents, then only those listed agents are allowed to repeat. If set to False, then no speakers are allowed to repeat.
- allow_repeat_speaker: whether to allow the same speaker to speak consecutively. Default is None, which is defaulted to True if allowed_or_disallowed_speaker_order is None, in which case all speakers are allowed to speak consecutively. If allow_repeat_speaker is a list of Agents, then only those listed agents are allowed to repeat. If set to False, then no speakers are allowed to repeat. allow_repeat_speaker and allowed_or_disallowed_speaker_order are mutually exclusive.
- allowed_or_disallowed_speaker_order: a dictionary of keys and list as values. The keys are the names of the agents, and the values are the agents that the key agent can transition to. Default is None, in which case a fully connected allowed_speaker_order_dict is assumed. allow_repeat_speaker and allowed_or_disallowed_speaker_order are mutually exclusive.
- speaker_order_type: whether the speaker_order_type is a dictionary containing lists of allowed agents or disallowed agents. allowed means the allowed_or_disallowed_speaker_order is a dictionary containing lists of allowed agents. If set to disallowed, then the allowed_or_disallowed_speaker_order is a dictionary containing lists of disallowed agents. Must be supplied if allowed_or_disallowed_speaker_order is not None.
"""

agents: List[Agent]
Expand All @@ -39,9 +52,93 @@ class GroupChat:
admin_name: Optional[str] = "Admin"
func_call_filter: Optional[bool] = True
speaker_selection_method: Optional[str] = "auto"
allow_repeat_speaker: Optional[Union[bool, List[Agent]]] = True
allow_repeat_speaker: Optional[
Union[bool, List[Agent]]
] = None # It would be set to True if allowed_or_disallowed_speaker_order is None
allowed_or_disallowed_speaker_order: Optional[Dict] = None
speaker_order_type: Optional[str] = None

_VALID_SPEAKER_SELECTION_METHODS = ["auto", "manual", "random", "round_robin"]
_VALID_SPEAKER_ORDER_TYPE = ["allowed", "disallowed", None]

allowed_speaker_order_dict: Dict = field(init=False)

def __post_init__(self):
# Post init steers clears of the automatically generated __init__ method from dataclass
# Here, we create allowed_speaker_order_dict from the supplied allowed_or_disallowed_speaker_order and is_allowed_graph, and lastly checks for validity.

# Check input
if self.speaker_order_type is not None:
self.speaker_order_type = self.speaker_order_type.lower()

assert self.speaker_order_type in self._VALID_SPEAKER_ORDER_TYPE, (
f"GroupChat speaker_order_type is set to '{self.speaker_order_type}'. "
f"It should be one of {self._VALID_SPEAKER_ORDER_TYPE} (case insensitive). "
)

# If both self.allowed_or_disallowed_speaker_order is None and self.allow_repeat_speaker is None, set allow_repeat_speaker to True to ensure backward compatibility
# Discussed in https://github.com/microsoft/autogen/pull/857#discussion_r1451541204
if self.allowed_or_disallowed_speaker_order is None and self.allow_repeat_speaker is None:
self.allow_repeat_speaker = True

# self.allowed_or_disallowed_speaker_order and self.allow_repeat_speaker are mutually exclusive parameters.
# Discussed in https://github.com/microsoft/autogen/pull/857#discussion_r1451266661
if self.allowed_or_disallowed_speaker_order is not None and self.allow_repeat_speaker is not None:
raise ValueError(
"GroupChat allowed_or_disallowed_speaker_order and allow_repeat_speaker cannot both be not None. "
joshkyh marked this conversation as resolved.
Show resolved Hide resolved
"Please set one of them to None."
)

# Asks the user to specify whether the speaker_order_type is allowed or disallowed if speaker_order_type is supplied
# Discussed in https://github.com/microsoft/autogen/pull/857#discussion_r1451259524
if self.allowed_or_disallowed_speaker_order is not None and self.allowed_or_disallowed_speaker_order is None:
sonichi marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(
"GroupChat allowed_or_disallowed_speaker_order is not None, but speaker_order_type is None. "
"Please set speaker_order_type to either 'allowed' or 'disallowed'."
)

# Inferring self.allowed_speaker_order_dict
# Create self.allowed_speaker_order_dict if allowed_or_disallowed_speaker_order is None, using allow_repeat_speaker
if self.allowed_or_disallowed_speaker_order is None:
self.allowed_speaker_order_dict = {}

# Create a fully connected allowed_speaker_order_dict not including self loops
for agent in self.agents:
self.allowed_speaker_order_dict[agent.name] = [
other_agent for other_agent in self.agents if other_agent != agent
]

# If self.allow_repeat_speaker is True, add self loops to all agents
if self.allow_repeat_speaker:
for agent in self.agents:
self.allowed_speaker_order_dict[agent.name].append(agent)

# Else if self.allow_repeat_speaker is a list of Agents, add self loops to the agents in the list
elif isinstance(self.allow_repeat_speaker, list):
for agent in self.allow_repeat_speaker:
self.allowed_speaker_order_dict[agent.name].append(agent)

# Create self.allowed_speaker_order_dict if allowed_or_disallowed_speaker_order is not None, using allowed_or_disallowed_speaker_order
elif self.allowed_or_disallowed_speaker_order is not None:
joshkyh marked this conversation as resolved.
Show resolved Hide resolved
# Process based on is_allowed_graph
if self.speaker_order_type == "allowed":
self.allowed_speaker_order_dict = self.allowed_or_disallowed_speaker_order
elif self.speaker_order_type == "disallowed":
joshkyh marked this conversation as resolved.
Show resolved Hide resolved
# Logic for processing disallowed allowed_or_disallowed_speaker_order to allowed_speaker_order_dict
self.allowed_speaker_order_dict = invert_disallowed_to_allowed(
self.allowed_or_disallowed_speaker_order, self.agents
)

# Inferring self.allow_repeat_speaker from allowed_speaker_order_dict using has_self_loops
sonichi marked this conversation as resolved.
Show resolved Hide resolved
if self.allow_repeat_speaker is None:
self.allow_repeat_speaker = has_self_loops(self.allowed_speaker_order_dict)

# Check for validity
check_graph_validity(
allowed_speaker_order_dict=self.allowed_speaker_order_dict,
agents=self.agents,
allow_repeat_speaker=self.allow_repeat_speaker,
)

@property
def agent_names(self) -> List[str]:
Expand Down Expand Up @@ -132,7 +229,7 @@ def manual_select_speaker(self, agents: Optional[List[Agent]] = None) -> Union[A

def _prepare_and_select_agents(
self, last_speaker: Agent
) -> Tuple[Optional[Agent], List[Agent], Optional[List[Dict]]]:
) -> Tuple[Optional[Agent], List[Agent], Optional[List[Dict]], Optional[List[Dict]]]:
sonichi marked this conversation as resolved.
Show resolved Hide resolved
if self.speaker_selection_method.lower() not in self._VALID_SPEAKER_SELECTION_METHODS:
raise ValueError(
f"GroupChat speaker_selection_method is set to '{self.speaker_selection_method}'. "
Expand Down Expand Up @@ -194,13 +291,26 @@ def _prepare_and_select_agents(
# remove the last speaker from the list to avoid selecting the same speaker if allow_repeat_speaker is False
agents = agents if allow_repeat_speaker else [agent for agent in agents if agent != last_speaker]

# Filter agents with allowed_speaker_order_dict
# if last_speaker.name is not a key in allowed_speaker_order_dict, then no agents are eligible
if last_speaker.name not in self.allowed_speaker_order_dict:
raise NoEligibleSpeakerException(
f"Last speaker {last_speaker.name} is not in the allowed_speaker_order_dict."
)
else:
# Extract agent names from the list of agents
graph_eligible_agents_names = [agent.name for agent in self.allowed_speaker_order_dict[last_speaker.name]]
# Perform filtering
graph_eligible_agents = [agent for agent in agents if agent.name in graph_eligible_agents_names]

# Use the selected speaker selection method
select_speaker_messages = None
if self.speaker_selection_method.lower() == "manual":
selected_agent = self.manual_select_speaker(agents)
selected_agent = self.manual_select_speaker(graph_eligible_agents)
elif self.speaker_selection_method.lower() == "round_robin":
selected_agent = self.next_agent(last_speaker, agents)
selected_agent = self.next_agent(last_speaker, graph_eligible_agents)
elif self.speaker_selection_method.lower() == "random":
selected_agent = random.choice(agents)
selected_agent = random.choice(graph_eligible_agents)
else:
selected_agent = None
select_speaker_messages = self.messages.copy()
Expand All @@ -210,11 +320,11 @@ def _prepare_and_select_agents(
if select_speaker_messages[-1].get("tool_calls", False):
select_speaker_messages[-1] = dict(select_speaker_messages[-1], tool_calls=None)
select_speaker_messages = select_speaker_messages + [
{"role": "system", "content": self.select_speaker_prompt(agents)}
{"role": "system", "content": self.select_speaker_prompt(graph_eligible_agents)}
]
return selected_agent, agents, select_speaker_messages
return selected_agent, graph_eligible_agents, select_speaker_messages

def select_speaker(self, last_speaker: Agent, selector: ConversableAgent):
def select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Agent:
"""Select the next speaker."""
selected_agent, agents, messages = self._prepare_and_select_agents(last_speaker)
if selected_agent:
Expand All @@ -224,7 +334,7 @@ def select_speaker(self, last_speaker: Agent, selector: ConversableAgent):
final, name = selector.generate_oai_reply(messages)
return self._finalize_speaker(last_speaker, final, name, agents)

async def a_select_speaker(self, last_speaker: Agent, selector: ConversableAgent):
async def a_select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Agent:
"""Select the next speaker."""
selected_agent, agents, messages = self._prepare_and_select_agents(last_speaker)
if selected_agent:
Expand Down Expand Up @@ -372,6 +482,10 @@ def run_chat(
else:
# admin agent is not found in the participants
raise
except NoEligibleSpeakerException:
# No eligible speaker, terminate the conversation
break

if reply is None:
break
# The speaker sends the message without requesting a reply
Expand Down
Loading
Loading