[BREAKING] Python: Move single-config fluent methods to constructor parameters (#3693)

* Move single-config fluent methods to constructor parameters

* Updates

* Adjust magentic and group chat
This commit is contained in:
Evan Mattson
2026-02-07 06:01:52 +00:00
committed by GitHub
parent 5d355ac507
commit 74ac470a56
132 changed files with 1341 additions and 2386 deletions
@@ -178,13 +178,12 @@ async def test_group_chat_builder_basic_flow() -> None:
alpha = StubAgent("alpha", "ack from alpha")
beta = StubAgent("beta", "ack from beta")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha, beta])
.with_max_rounds(2) # Limit rounds to prevent infinite loop
.build()
)
workflow = GroupChatBuilder(
participants=[alpha, beta],
max_rounds=2, # Limit rounds to prevent infinite loop
selection_func=selector,
orchestrator_name="manager",
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("coordinate task", stream=True):
@@ -205,13 +204,12 @@ async def test_group_chat_as_agent_accepts_conversation() -> None:
alpha = StubAgent("alpha", "ack from alpha")
beta = StubAgent("beta", "ack from beta")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha, beta])
.with_max_rounds(2) # Limit rounds to prevent infinite loop
.build()
)
workflow = GroupChatBuilder(
participants=[alpha, beta],
max_rounds=2, # Limit rounds to prevent infinite loop
selection_func=selector,
orchestrator_name="manager",
).build()
agent = workflow.as_agent(name="group-chat-agent")
conversation = [
@@ -233,64 +231,47 @@ class TestGroupChatBuilder:
"""Test that building without a manager raises ValueError."""
agent = StubAgent("test", "response")
builder = GroupChatBuilder().participants([agent])
builder = GroupChatBuilder(participants=[agent])
with pytest.raises(
ValueError, match=r"No orchestrator has been configured\. Call with_orchestrator\(\) to set one\."
ValueError,
match=r"No orchestrator has been configured\.",
):
builder.build()
def test_build_without_participants_raises_error(self) -> None:
"""Test that building without participants raises ValueError."""
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
"""Test that constructing without participants raises ValueError."""
with pytest.raises(
ValueError,
match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first\.",
match=r"Either participants or participant_factories must be provided\.",
):
builder.build()
GroupChatBuilder()
def test_duplicate_manager_configuration_raises_error(self) -> None:
"""Test that configuring multiple managers raises ValueError."""
"""Test that configuring multiple orchestrator options raises ValueError."""
agent = StubAgent("test", "response")
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(
ValueError,
match=r"A selection function has already been configured\. Call with_orchestrator\(\.\.\.\) once only\.",
match=r"Exactly one of",
):
builder.with_orchestrator(selection_func=selector)
GroupChatBuilder(participants=[agent], selection_func=selector, orchestrator_agent=StubManagerAgent())
def test_empty_participants_raises_error(self) -> None:
"""Test that empty participants list raises ValueError."""
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="participants cannot be empty"):
builder.participants([])
GroupChatBuilder(participants=[])
def test_duplicate_participant_names_raises_error(self) -> None:
"""Test that duplicate participant names raise ValueError."""
agent1 = StubAgent("test", "response1")
agent2 = StubAgent("test", "response2")
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="Duplicate participant name 'test'"):
builder.participants([agent1, agent2])
GroupChatBuilder(participants=[agent1, agent2])
def test_agent_without_name_raises_error(self) -> None:
"""Test that agent without name attribute raises ValueError."""
@@ -315,25 +296,15 @@ class TestGroupChatBuilder:
agent = AgentWithoutName()
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="SupportsAgentRun participants must have a non-empty name"):
builder.participants([agent])
GroupChatBuilder(participants=[agent])
def test_empty_participant_name_raises_error(self) -> None:
"""Test that empty participant name raises ValueError."""
agent = StubAgent("", "response") # Agent with empty name
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="SupportsAgentRun participants must have a non-empty name"):
builder.participants([agent])
GroupChatBuilder(participants=[agent])
class TestGroupChatWorkflow:
@@ -350,13 +321,11 @@ class TestGroupChatWorkflow:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(2) # Limit to 2 rounds
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
max_rounds=2, # Limit to 2 rounds
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test task", stream=True):
@@ -385,13 +354,11 @@ class TestGroupChatWorkflow:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_termination_condition(termination_condition)
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
termination_condition=termination_condition,
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test task", stream=True):
@@ -413,13 +380,11 @@ class TestGroupChatWorkflow:
manager = StubManagerAgent()
worker = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(agent=manager)
.participants([worker])
.with_termination_condition(lambda conv: any(msg.author_name == "agent" for msg in conv))
.build()
)
workflow = GroupChatBuilder(
participants=[worker],
termination_condition=lambda conv: any(msg.author_name == "agent" for msg in conv),
orchestrator_agent=manager,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test task", stream=True):
@@ -441,7 +406,7 @@ class TestGroupChatWorkflow:
agent = StubAgent("agent", "response")
workflow = GroupChatBuilder().with_orchestrator(selection_func=selector).participants([agent]).build()
workflow = GroupChatBuilder(participants=[agent], selection_func=selector).build()
with pytest.raises(RuntimeError, match="Selection function returned unknown participant 'unknown_agent'"):
async for _ in workflow.run("test task", stream=True):
@@ -460,14 +425,12 @@ class TestCheckpointing:
agent = StubAgent("agent", "response")
storage = InMemoryCheckpointStorage()
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.with_checkpointing(storage)
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
max_rounds=1,
checkpoint_storage=storage,
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test task", stream=True):
@@ -490,13 +453,7 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build()
with pytest.raises(ValueError, match="At least one ChatMessage is required to start the group chat workflow."):
async for _ in workflow.run([], stream=True):
@@ -514,13 +471,7 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test string", stream=True):
@@ -543,13 +494,7 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run(task_message, stream=True):
@@ -575,13 +520,7 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run(conversation, stream=True):
@@ -607,13 +546,11 @@ class TestRoundLimitEnforcement:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1) # Very low limit
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
max_rounds=1, # Very low limit
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test", stream=True):
@@ -642,13 +579,11 @@ class TestRoundLimitEnforcement:
agent = StubAgent("agent", "response from agent")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1) # Hit limit after first response
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
max_rounds=1, # Hit limit after first response
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test", stream=True):
@@ -674,13 +609,7 @@ async def test_group_chat_checkpoint_runtime_only() -> None:
agent_b = StubAgent("agentB", "Reply from B")
selector = make_sequence_selector()
wf = (
GroupChatBuilder()
.participants([agent_a, agent_b])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
.build()
)
wf = GroupChatBuilder(participants=[agent_a, agent_b], max_rounds=2, selection_func=selector).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True):
@@ -712,14 +641,12 @@ async def test_group_chat_checkpoint_runtime_overrides_buildtime() -> None:
agent_b = StubAgent("agentB", "Reply from B")
selector = make_sequence_selector()
wf = (
GroupChatBuilder()
.participants([agent_a, agent_b])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
.with_checkpointing(buildtime_storage)
.build()
)
wf = GroupChatBuilder(
participants=[agent_a, agent_b],
max_rounds=2,
checkpoint_storage=buildtime_storage,
selection_func=selector,
).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True):
if ev.type == "output":
@@ -759,10 +686,12 @@ async def test_group_chat_with_request_info_filtering():
return "alpha"
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha, beta])
.with_max_rounds(2)
GroupChatBuilder(
participants=[alpha, beta],
max_rounds=2,
selection_func=selector,
orchestrator_name="manager",
)
.with_request_info(agents=["beta"]) # Only pause before beta runs
.build()
)
@@ -811,10 +740,12 @@ async def test_group_chat_with_request_info_no_filter_pauses_all():
return "alpha"
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha])
.with_max_rounds(1)
GroupChatBuilder(
participants=[alpha],
max_rounds=1,
selection_func=selector,
orchestrator_name="manager",
)
.with_request_info() # No filter - pause for all
.build()
)
@@ -833,12 +764,13 @@ async def test_group_chat_with_request_info_no_filter_pauses_all():
def test_group_chat_builder_with_request_info_returns_self():
"""Test that with_request_info() returns self for method chaining."""
builder = GroupChatBuilder()
agent = StubAgent("test", "response")
builder = GroupChatBuilder(participants=[agent])
result = builder.with_request_info()
assert result is builder
# Also test with agents parameter
builder2 = GroupChatBuilder()
builder2 = GroupChatBuilder(participants=[agent])
result2 = builder2.with_request_info(agents=["test"])
assert result2 is builder2
@@ -853,47 +785,41 @@ def test_group_chat_builder_rejects_empty_participant_factories():
return list(state.participants.keys())[0]
with pytest.raises(ValueError, match=r"participant_factories cannot be empty"):
GroupChatBuilder().register_participants([])
GroupChatBuilder(participant_factories=[])
with pytest.raises(
ValueError,
match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first\.",
match=r"Either participants or participant_factories must be provided\.",
):
GroupChatBuilder().with_orchestrator(selection_func=selector).build()
GroupChatBuilder()
def test_group_chat_builder_rejects_mixing_participants_and_factories():
"""Test that mixing .participants() and .register_participants() raises an error."""
"""Test that passing both participants and participant_factories to the constructor raises an error."""
alpha = StubAgent("alpha", "reply from alpha")
# Case 1: participants first, then register_participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
GroupChatBuilder().participants([alpha]).register_participants([lambda: StubAgent("beta", "reply from beta")])
# Case 2: register_participants first, then participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
GroupChatBuilder().register_participants([lambda: alpha]).participants([StubAgent("beta", "reply from beta")])
def test_group_chat_builder_rejects_multiple_calls_to_register_participants():
"""Test that multiple calls to .register_participants() raises an error."""
with pytest.raises(
ValueError, match=r"register_participants\(\) has already been called on this builder instance."
):
(
GroupChatBuilder()
.register_participants([lambda: StubAgent("alpha", "reply from alpha")])
.register_participants([lambda: StubAgent("beta", "reply from beta")])
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
GroupChatBuilder(
participants=[alpha],
participant_factories=[lambda: StubAgent("beta", "reply from beta")],
)
def test_group_chat_builder_rejects_multiple_calls_to_participants():
"""Test that multiple calls to .participants() raises an error."""
with pytest.raises(ValueError, match="participants have already been set"):
(
GroupChatBuilder()
.participants([StubAgent("alpha", "reply from alpha")])
.participants([StubAgent("beta", "reply from beta")])
def test_group_chat_builder_rejects_both_factories_and_participants():
"""Test that passing both participant_factories and participants raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
GroupChatBuilder(
participant_factories=[lambda: StubAgent("alpha", "reply from alpha")],
participants=[StubAgent("beta", "reply from beta")],
)
def test_group_chat_builder_rejects_both_participants_and_factories():
"""Test that passing both participants and participant_factories raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
GroupChatBuilder(
participants=[StubAgent("alpha", "reply from alpha")],
participant_factories=[lambda: StubAgent("beta", "reply from beta")],
)
@@ -913,13 +839,11 @@ async def test_group_chat_with_participant_factories():
selector = make_sequence_selector()
workflow = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
.build()
)
workflow = GroupChatBuilder(
participant_factories=[create_alpha, create_beta],
max_rounds=2,
selection_func=selector,
).build()
# Factories should be called during build
assert call_count == 2
@@ -948,12 +872,7 @@ async def test_group_chat_participant_factories_reusable_builder():
selector = make_sequence_selector()
builder = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
)
builder = GroupChatBuilder(participant_factories=[create_alpha, create_beta], max_rounds=2, selection_func=selector)
# Build first workflow
wf1 = builder.build()
@@ -980,14 +899,12 @@ async def test_group_chat_participant_factories_with_checkpointing():
selector = make_sequence_selector()
workflow = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(selection_func=selector)
.with_checkpointing(storage)
.with_max_rounds(2)
.build()
)
workflow = GroupChatBuilder(
participant_factories=[create_alpha, create_beta],
checkpoint_storage=storage,
max_rounds=2,
selection_func=selector,
).build()
outputs: list[WorkflowEvent] = []
async for event in workflow.run("checkpoint test", stream=True):
@@ -1014,16 +931,15 @@ def test_group_chat_builder_rejects_multiple_orchestrator_configurations():
def agent_factory() -> ChatAgent:
return cast(ChatAgent, StubManagerAgent())
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
agent = StubAgent("test", "response")
# Already has a selection_func, should fail on second call
with pytest.raises(ValueError, match=r"A selection function has already been configured"):
builder.with_orchestrator(selection_func=selector)
# Both selection_func and orchestrator_agent provided simultaneously - should fail
with pytest.raises(ValueError, match=r"Exactly one of"):
GroupChatBuilder(participants=[agent], selection_func=selector, orchestrator_agent=StubManagerAgent())
# Test with agent_factory
builder2 = GroupChatBuilder().with_orchestrator(agent=agent_factory)
with pytest.raises(ValueError, match=r"A factory has already been configured"):
builder2.with_orchestrator(agent=agent_factory)
# Test with agent_factory - already has factory, should fail with second config
with pytest.raises(ValueError, match=r"Exactly one of"):
GroupChatBuilder(participants=[agent], orchestrator_agent=agent_factory, selection_func=selector)
def test_group_chat_builder_requires_exactly_one_orchestrator_option():
@@ -1035,13 +951,15 @@ def test_group_chat_builder_requires_exactly_one_orchestrator_option():
def agent_factory() -> ChatAgent:
return cast(ChatAgent, StubManagerAgent())
# No options provided
with pytest.raises(ValueError, match="Exactly one of"):
GroupChatBuilder().with_orchestrator() # type: ignore
agent = StubAgent("test", "response")
# No orchestrator options provided - only fails at build() time
with pytest.raises(ValueError, match="No orchestrator has been configured"):
GroupChatBuilder(participants=[agent]).build()
# Multiple options provided
with pytest.raises(ValueError, match="Exactly one of"):
GroupChatBuilder().with_orchestrator(selection_func=selector, agent=agent_factory) # type: ignore
GroupChatBuilder(participants=[agent], selection_func=selector, orchestrator_agent=agent_factory)
async def test_group_chat_with_orchestrator_factory_returning_chat_agent():
@@ -1112,7 +1030,7 @@ async def test_group_chat_with_orchestrator_factory_returning_chat_agent():
alpha = StubAgent("alpha", "reply from alpha")
beta = StubAgent("beta", "reply from beta")
workflow = GroupChatBuilder().participants([alpha, beta]).with_orchestrator(agent=agent_factory).build()
workflow = GroupChatBuilder(participants=[alpha, beta], orchestrator_agent=agent_factory).build()
# Factory should be called during build
assert factory_call_count == 1
@@ -1156,7 +1074,7 @@ def test_group_chat_with_orchestrator_factory_returning_base_orchestrator():
alpha = StubAgent("alpha", "reply from alpha")
workflow = GroupChatBuilder().participants([alpha]).with_orchestrator(orchestrator=orchestrator_factory).build()
workflow = GroupChatBuilder(participants=[alpha], orchestrator=orchestrator_factory).build()
# Factory should be called during build
assert factory_call_count == 1
@@ -1176,7 +1094,7 @@ async def test_group_chat_orchestrator_factory_reusable_builder():
alpha = StubAgent("alpha", "reply from alpha")
beta = StubAgent("beta", "reply from beta")
builder = GroupChatBuilder().participants([alpha, beta]).with_orchestrator(agent=agent_factory)
builder = GroupChatBuilder(participants=[alpha, beta], orchestrator_agent=agent_factory)
# Build first workflow
wf1 = builder.build()
@@ -1202,13 +1120,13 @@ def test_group_chat_orchestrator_factory_invalid_return_type():
TypeError,
match=r"Orchestrator factory must return ChatAgent or BaseGroupChatOrchestrator instance",
):
(GroupChatBuilder().participants([alpha]).with_orchestrator(orchestrator=invalid_factory).build())
GroupChatBuilder(participants=[alpha], orchestrator=invalid_factory).build()
with pytest.raises(
TypeError,
match=r"Orchestrator factory must return ChatAgent or BaseGroupChatOrchestrator instance",
):
(GroupChatBuilder().participants([alpha]).with_orchestrator(agent=invalid_factory).build())
GroupChatBuilder(participants=[alpha], orchestrator_agent=invalid_factory).build()
def test_group_chat_with_both_participant_and_orchestrator_factories():
@@ -1231,12 +1149,10 @@ def test_group_chat_with_both_participant_and_orchestrator_factories():
agent_factory_call_count += 1
return cast(ChatAgent, StubManagerAgent())
workflow = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(agent=agent_factory)
.build()
)
workflow = GroupChatBuilder(
participant_factories=[create_alpha, create_beta],
orchestrator_agent=agent_factory,
).build()
# All factories should be called during build
assert participant_factory_call_count == 2
@@ -1268,9 +1184,7 @@ async def test_group_chat_factories_reusable_for_multiple_workflows():
agent_factory_call_count += 1
return cast(ChatAgent, StubManagerAgent())
builder = (
GroupChatBuilder().register_participants([create_alpha, create_beta]).with_orchestrator(agent=agent_factory)
)
builder = GroupChatBuilder(participant_factories=[create_alpha, create_beta], orchestrator_agent=agent_factory)
# Build first workflow
wf1 = builder.build()