mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[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:
committed by
GitHub
Unverified
parent
5d355ac507
commit
74ac470a56
@@ -39,14 +39,14 @@ class _FakeAgentExec(Executor):
|
||||
|
||||
def test_concurrent_builder_rejects_empty_participants() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
ConcurrentBuilder().participants([])
|
||||
ConcurrentBuilder(participants=[])
|
||||
|
||||
|
||||
def test_concurrent_builder_rejects_duplicate_executors() -> None:
|
||||
a = _FakeAgentExec("dup", "A")
|
||||
b = _FakeAgentExec("dup", "B") # same executor id
|
||||
with pytest.raises(ValueError):
|
||||
ConcurrentBuilder().participants([a, b])
|
||||
ConcurrentBuilder(participants=[a, b])
|
||||
|
||||
|
||||
def test_concurrent_builder_rejects_duplicate_executors_from_factories() -> None:
|
||||
@@ -58,43 +58,35 @@ def test_concurrent_builder_rejects_duplicate_executors_from_factories() -> None
|
||||
def create_dup2() -> Executor:
|
||||
return _FakeAgentExec("dup", "B") # same executor id
|
||||
|
||||
builder = ConcurrentBuilder().register_participants([create_dup1, create_dup2])
|
||||
builder = ConcurrentBuilder(participant_factories=[create_dup1, create_dup2])
|
||||
with pytest.raises(ValueError, match="Duplicate executor ID 'dup' detected in workflow."):
|
||||
builder.build()
|
||||
|
||||
|
||||
def test_concurrent_builder_rejects_mixed_participants_and_factories() -> None:
|
||||
"""Test that mixing .participants() and .register_participants() raises an error."""
|
||||
# Case 1: participants first, then register_participants
|
||||
with pytest.raises(ValueError, match="Cannot mix .participants"):
|
||||
(
|
||||
ConcurrentBuilder()
|
||||
.participants([_FakeAgentExec("a", "A")])
|
||||
.register_participants([lambda: _FakeAgentExec("b", "B")])
|
||||
)
|
||||
|
||||
# Case 2: register_participants first, then participants
|
||||
with pytest.raises(ValueError, match="Cannot mix .participants"):
|
||||
(
|
||||
ConcurrentBuilder()
|
||||
.register_participants([lambda: _FakeAgentExec("a", "A")])
|
||||
.participants([_FakeAgentExec("b", "B")])
|
||||
"""Test that passing both participants and participant_factories to the constructor raises an error."""
|
||||
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
|
||||
ConcurrentBuilder(
|
||||
participants=[_FakeAgentExec("a", "A")],
|
||||
participant_factories=[lambda: _FakeAgentExec("b", "B")],
|
||||
)
|
||||
|
||||
|
||||
def test_concurrent_builder_rejects_multiple_calls_to_participants() -> None:
|
||||
"""Test that multiple calls to .participants() raises an error."""
|
||||
with pytest.raises(ValueError, match=r"participants\(\) has already been called"):
|
||||
(ConcurrentBuilder().participants([_FakeAgentExec("a", "A")]).participants([_FakeAgentExec("b", "B")]))
|
||||
def test_concurrent_builder_rejects_both_participants_and_factories() -> None:
|
||||
"""Test that passing both participants and participant_factories raises an error."""
|
||||
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
|
||||
ConcurrentBuilder(
|
||||
participants=[_FakeAgentExec("a", "A")],
|
||||
participant_factories=[lambda: _FakeAgentExec("b", "B")],
|
||||
)
|
||||
|
||||
|
||||
def test_concurrent_builder_rejects_multiple_calls_to_register_participants() -> None:
|
||||
"""Test that multiple calls to .register_participants() raises an error."""
|
||||
with pytest.raises(ValueError, match=r"register_participants\(\) has already been called"):
|
||||
(
|
||||
ConcurrentBuilder()
|
||||
.register_participants([lambda: _FakeAgentExec("a", "A")])
|
||||
.register_participants([lambda: _FakeAgentExec("b", "B")])
|
||||
def test_concurrent_builder_rejects_both_factories_and_participants() -> None:
|
||||
"""Test that passing both participant_factories and participants raises an error."""
|
||||
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
|
||||
ConcurrentBuilder(
|
||||
participant_factories=[lambda: _FakeAgentExec("a", "A")],
|
||||
participants=[_FakeAgentExec("b", "B")],
|
||||
)
|
||||
|
||||
|
||||
@@ -104,7 +96,7 @@ async def test_concurrent_default_aggregator_emits_single_user_and_assistants()
|
||||
e2 = _FakeAgentExec("agentB", "Beta")
|
||||
e3 = _FakeAgentExec("agentC", "Gamma")
|
||||
|
||||
wf = ConcurrentBuilder().participants([e1, e2, e3]).build()
|
||||
wf = ConcurrentBuilder(participants=[e1, e2, e3]).build()
|
||||
|
||||
completed = False
|
||||
output: list[ChatMessage] | None = None
|
||||
@@ -142,7 +134,7 @@ async def test_concurrent_custom_aggregator_callback_is_used() -> None:
|
||||
texts.append(msgs[-1].text if msgs else "")
|
||||
return " | ".join(sorted(texts))
|
||||
|
||||
wf = ConcurrentBuilder().participants([e1, e2]).with_aggregator(summarize).build()
|
||||
wf = ConcurrentBuilder(participants=[e1, e2]).with_aggregator(summarize).build()
|
||||
|
||||
completed = False
|
||||
output: str | None = None
|
||||
@@ -173,7 +165,7 @@ async def test_concurrent_custom_aggregator_sync_callback_is_used() -> None:
|
||||
texts.append(msgs[-1].text if msgs else "")
|
||||
return " | ".join(sorted(texts))
|
||||
|
||||
wf = ConcurrentBuilder().participants([e1, e2]).with_aggregator(summarize_sync).build()
|
||||
wf = ConcurrentBuilder(participants=[e1, e2]).with_aggregator(summarize_sync).build()
|
||||
|
||||
completed = False
|
||||
output: str | None = None
|
||||
@@ -198,7 +190,7 @@ def test_concurrent_custom_aggregator_uses_callback_name_for_id() -> None:
|
||||
def summarize(results: list[AgentExecutorResponse]) -> str: # type: ignore[override]
|
||||
return str(len(results))
|
||||
|
||||
wf = ConcurrentBuilder().participants([e1, e2]).with_aggregator(summarize).build()
|
||||
wf = ConcurrentBuilder(participants=[e1, e2]).with_aggregator(summarize).build()
|
||||
|
||||
assert "summarize" in wf.executors
|
||||
aggregator = wf.executors["summarize"]
|
||||
@@ -221,7 +213,7 @@ async def test_concurrent_with_aggregator_executor_instance() -> None:
|
||||
e2 = _FakeAgentExec("agentB", "Two")
|
||||
|
||||
aggregator_instance = CustomAggregator(id="instance_aggregator")
|
||||
wf = ConcurrentBuilder().participants([e1, e2]).with_aggregator(aggregator_instance).build()
|
||||
wf = ConcurrentBuilder(participants=[e1, e2]).with_aggregator(aggregator_instance).build()
|
||||
|
||||
completed = False
|
||||
output: str | None = None
|
||||
@@ -255,8 +247,7 @@ async def test_concurrent_with_aggregator_executor_factory() -> None:
|
||||
e2 = _FakeAgentExec("agentB", "Two")
|
||||
|
||||
wf = (
|
||||
ConcurrentBuilder()
|
||||
.participants([e1, e2])
|
||||
ConcurrentBuilder(participants=[e1, e2])
|
||||
.register_aggregator(lambda: CustomAggregator(id="custom_aggregator"))
|
||||
.build()
|
||||
)
|
||||
@@ -295,7 +286,7 @@ async def test_concurrent_with_aggregator_executor_factory_with_default_id() ->
|
||||
e1 = _FakeAgentExec("agentA", "One")
|
||||
e2 = _FakeAgentExec("agentB", "Two")
|
||||
|
||||
wf = ConcurrentBuilder().participants([e1, e2]).register_aggregator(CustomAggregator).build()
|
||||
wf = ConcurrentBuilder(participants=[e1, e2]).register_aggregator(CustomAggregator).build()
|
||||
|
||||
completed = False
|
||||
output: str | None = None
|
||||
@@ -320,7 +311,11 @@ def test_concurrent_builder_rejects_multiple_calls_to_with_aggregator() -> None:
|
||||
return str(len(results))
|
||||
|
||||
with pytest.raises(ValueError, match=r"with_aggregator\(\) has already been called"):
|
||||
(ConcurrentBuilder().with_aggregator(summarize).with_aggregator(summarize))
|
||||
(
|
||||
ConcurrentBuilder(participants=[_FakeAgentExec("a", "A")])
|
||||
.with_aggregator(summarize)
|
||||
.with_aggregator(summarize)
|
||||
)
|
||||
|
||||
|
||||
def test_concurrent_builder_rejects_multiple_calls_to_register_aggregator() -> None:
|
||||
@@ -331,7 +326,7 @@ def test_concurrent_builder_rejects_multiple_calls_to_register_aggregator() -> N
|
||||
|
||||
with pytest.raises(ValueError, match=r"register_aggregator\(\) has already been called"):
|
||||
(
|
||||
ConcurrentBuilder()
|
||||
ConcurrentBuilder(participants=[_FakeAgentExec("a", "A")])
|
||||
.register_aggregator(lambda: CustomAggregator(id="agg1"))
|
||||
.register_aggregator(lambda: CustomAggregator(id="agg2"))
|
||||
)
|
||||
@@ -346,7 +341,7 @@ async def test_concurrent_checkpoint_resume_round_trip() -> None:
|
||||
_FakeAgentExec("agentC", "Gamma"),
|
||||
)
|
||||
|
||||
wf = ConcurrentBuilder().participants(list(participants)).with_checkpointing(storage).build()
|
||||
wf = ConcurrentBuilder(participants=list(participants), checkpoint_storage=storage).build()
|
||||
|
||||
baseline_output: list[ChatMessage] | None = None
|
||||
async for ev in wf.run("checkpoint concurrent", stream=True):
|
||||
@@ -370,7 +365,7 @@ async def test_concurrent_checkpoint_resume_round_trip() -> None:
|
||||
_FakeAgentExec("agentB", "Beta"),
|
||||
_FakeAgentExec("agentC", "Gamma"),
|
||||
)
|
||||
wf_resume = ConcurrentBuilder().participants(list(resumed_participants)).with_checkpointing(storage).build()
|
||||
wf_resume = ConcurrentBuilder(participants=list(resumed_participants), checkpoint_storage=storage).build()
|
||||
|
||||
resumed_output: list[ChatMessage] | None = None
|
||||
async for ev in wf_resume.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
|
||||
@@ -392,7 +387,7 @@ async def test_concurrent_checkpoint_runtime_only() -> None:
|
||||
storage = InMemoryCheckpointStorage()
|
||||
|
||||
agents = [_FakeAgentExec(id="agent1", reply_text="A1"), _FakeAgentExec(id="agent2", reply_text="A2")]
|
||||
wf = ConcurrentBuilder().participants(agents).build()
|
||||
wf = ConcurrentBuilder(participants=agents).build()
|
||||
|
||||
baseline_output: list[ChatMessage] | None = None
|
||||
async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True):
|
||||
@@ -413,7 +408,7 @@ async def test_concurrent_checkpoint_runtime_only() -> None:
|
||||
)
|
||||
|
||||
resumed_agents = [_FakeAgentExec(id="agent1", reply_text="A1"), _FakeAgentExec(id="agent2", reply_text="A2")]
|
||||
wf_resume = ConcurrentBuilder().participants(resumed_agents).build()
|
||||
wf_resume = ConcurrentBuilder(participants=resumed_agents).build()
|
||||
|
||||
resumed_output: list[ChatMessage] | None = None
|
||||
async for ev in wf_resume.run(
|
||||
@@ -442,7 +437,7 @@ async def test_concurrent_checkpoint_runtime_overrides_buildtime() -> None:
|
||||
runtime_storage = FileCheckpointStorage(temp_dir2)
|
||||
|
||||
agents = [_FakeAgentExec(id="agent1", reply_text="A1"), _FakeAgentExec(id="agent2", reply_text="A2")]
|
||||
wf = ConcurrentBuilder().participants(agents).with_checkpointing(buildtime_storage).build()
|
||||
wf = ConcurrentBuilder(participants=agents, checkpoint_storage=buildtime_storage).build()
|
||||
|
||||
baseline_output: list[ChatMessage] | None = None
|
||||
async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True):
|
||||
@@ -462,7 +457,7 @@ async def test_concurrent_checkpoint_runtime_overrides_buildtime() -> None:
|
||||
|
||||
def test_concurrent_builder_rejects_empty_participant_factories() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
ConcurrentBuilder().register_participants([])
|
||||
ConcurrentBuilder(participant_factories=[])
|
||||
|
||||
|
||||
async def test_concurrent_builder_reusable_after_build_with_participants() -> None:
|
||||
@@ -470,7 +465,7 @@ async def test_concurrent_builder_reusable_after_build_with_participants() -> No
|
||||
e1 = _FakeAgentExec("agentA", "One")
|
||||
e2 = _FakeAgentExec("agentB", "Two")
|
||||
|
||||
builder = ConcurrentBuilder().participants([e1, e2])
|
||||
builder = ConcurrentBuilder(participants=[e1, e2])
|
||||
|
||||
builder.build()
|
||||
|
||||
@@ -493,7 +488,7 @@ async def test_concurrent_builder_reusable_after_build_with_factories() -> None:
|
||||
call_count += 1
|
||||
return _FakeAgentExec("agentB", "Two")
|
||||
|
||||
builder = ConcurrentBuilder().register_participants([create_agent_executor_a, create_agent_executor_b])
|
||||
builder = ConcurrentBuilder(participant_factories=[create_agent_executor_a, create_agent_executor_b])
|
||||
|
||||
# Build the first workflow
|
||||
wf1 = builder.build()
|
||||
@@ -523,7 +518,7 @@ async def test_concurrent_with_register_participants() -> None:
|
||||
def create_agent3() -> Executor:
|
||||
return _FakeAgentExec("agentC", "Gamma")
|
||||
|
||||
wf = ConcurrentBuilder().register_participants([create_agent1, create_agent2, create_agent3]).build()
|
||||
wf = ConcurrentBuilder(participant_factories=[create_agent1, create_agent2, create_agent3]).build()
|
||||
|
||||
completed = False
|
||||
output: list[ChatMessage] | None = None
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -140,9 +140,11 @@ async def test_handoff():
|
||||
# Without explicitly defining handoffs, the builder will create connections
|
||||
# between all agents.
|
||||
workflow = (
|
||||
HandoffBuilder(participants=[triage, specialist, escalation])
|
||||
HandoffBuilder(
|
||||
participants=[triage, specialist, escalation],
|
||||
termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 2,
|
||||
)
|
||||
.with_start_agent(triage)
|
||||
.with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 2)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -166,7 +168,15 @@ async def test_autonomous_mode_yields_output_without_user_request():
|
||||
specialist = MockHandoffAgent(name="specialist")
|
||||
|
||||
workflow = (
|
||||
HandoffBuilder(participants=[triage, specialist])
|
||||
HandoffBuilder(
|
||||
participants=[triage, specialist],
|
||||
# This termination condition ensures the workflow runs through both agents.
|
||||
# First message is the user message to triage, second is triage's response, which
|
||||
# is a handoff to specialist, third is specialist's response that should not request
|
||||
# user input due to autonomous mode. Fourth message will come from the specialist
|
||||
# again and will trigger termination.
|
||||
termination_condition=lambda conv: len(conv) >= 4,
|
||||
)
|
||||
.with_start_agent(triage)
|
||||
# Since specialist has no handoff, the specialist will be generating normal responses.
|
||||
# With autonomous mode, this should continue until the termination condition is met.
|
||||
@@ -174,12 +184,6 @@ async def test_autonomous_mode_yields_output_without_user_request():
|
||||
agents=[specialist],
|
||||
turn_limits={resolve_agent_id(specialist): 1},
|
||||
)
|
||||
# This termination condition ensures the workflow runs through both agents.
|
||||
# First message is the user message to triage, second is triage's response, which
|
||||
# is a handoff to specialist, third is specialist's response that should not request
|
||||
# user input due to autonomous mode. Fourth message will come from the specialist
|
||||
# again and will trigger termination.
|
||||
.with_termination_condition(lambda conv: len(conv) >= 4)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -202,10 +206,9 @@ async def test_autonomous_mode_resumes_user_input_on_turn_limit():
|
||||
worker = MockHandoffAgent(name="worker")
|
||||
|
||||
workflow = (
|
||||
HandoffBuilder(participants=[triage, worker])
|
||||
HandoffBuilder(participants=[triage, worker], termination_condition=lambda conv: False)
|
||||
.with_start_agent(triage)
|
||||
.with_autonomous_mode(agents=[worker], turn_limits={resolve_agent_id(worker): 2})
|
||||
.with_termination_condition(lambda conv: False)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -246,9 +249,8 @@ async def test_handoff_async_termination_condition() -> None:
|
||||
worker = MockHandoffAgent(name="worker")
|
||||
|
||||
workflow = (
|
||||
HandoffBuilder(participants=[coordinator, worker])
|
||||
HandoffBuilder(participants=[coordinator, worker], termination_condition=async_termination)
|
||||
.with_start_agent(coordinator)
|
||||
.with_termination_condition(async_termination)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -537,9 +539,11 @@ async def test_handoff_with_participant_factories():
|
||||
return MockHandoffAgent(name="specialist")
|
||||
|
||||
workflow = (
|
||||
HandoffBuilder(participant_factories={"triage": create_triage, "specialist": create_specialist})
|
||||
HandoffBuilder(
|
||||
participant_factories={"triage": create_triage, "specialist": create_specialist},
|
||||
termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 2,
|
||||
)
|
||||
.with_start_agent("triage")
|
||||
.with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 2)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -607,12 +611,12 @@ async def test_handoff_with_participant_factories_and_add_handoff():
|
||||
"triage": create_triage,
|
||||
"specialist_a": create_specialist_a,
|
||||
"specialist_b": create_specialist_b,
|
||||
}
|
||||
},
|
||||
termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 3,
|
||||
)
|
||||
.with_start_agent("triage")
|
||||
.add_handoff("triage", ["specialist_a", "specialist_b"])
|
||||
.add_handoff("specialist_a", ["specialist_b"])
|
||||
.with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 3)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -650,10 +654,12 @@ async def test_handoff_participant_factories_with_checkpointing():
|
||||
return MockHandoffAgent(name="specialist")
|
||||
|
||||
workflow = (
|
||||
HandoffBuilder(participant_factories={"triage": create_triage, "specialist": create_specialist})
|
||||
HandoffBuilder(
|
||||
participant_factories={"triage": create_triage, "specialist": create_specialist},
|
||||
checkpoint_storage=storage,
|
||||
termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 2,
|
||||
)
|
||||
.with_start_agent("triage")
|
||||
.with_checkpointing(storage)
|
||||
.with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 2)
|
||||
.build()
|
||||
)
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ async def test_magentic_builder_returns_workflow_and_runs() -> None:
|
||||
manager = FakeManager()
|
||||
agent = StubAgent(manager.next_speaker_name, "first draft")
|
||||
|
||||
workflow = MagenticBuilder().participants([agent]).with_manager(manager=manager).build()
|
||||
workflow = MagenticBuilder(participants=[agent], manager=manager).build()
|
||||
|
||||
assert isinstance(workflow, Workflow)
|
||||
|
||||
@@ -212,7 +212,7 @@ async def test_magentic_as_agent_does_not_accept_conversation() -> None:
|
||||
manager = FakeManager()
|
||||
writer = StubAgent(manager.next_speaker_name, "summary response")
|
||||
|
||||
workflow = MagenticBuilder().participants([writer]).with_manager(manager=manager).build()
|
||||
workflow = MagenticBuilder(participants=[writer], manager=manager).build()
|
||||
|
||||
agent = workflow.as_agent(name="magentic-agent")
|
||||
conversation = [
|
||||
@@ -240,7 +240,7 @@ async def test_standard_manager_plan_and_replan_combined_ledger():
|
||||
|
||||
async def test_magentic_workflow_plan_review_approval_to_completion():
|
||||
manager = FakeManager()
|
||||
wf = MagenticBuilder().participants([DummyExec("agentA")]).with_manager(manager=manager).with_plan_review().build()
|
||||
wf = MagenticBuilder(participants=[DummyExec("agentA")], enable_plan_review=True, manager=manager).build()
|
||||
|
||||
req_event: WorkflowEvent | None = None
|
||||
async for ev in wf.run("do work", stream=True):
|
||||
@@ -278,13 +278,11 @@ async def test_magentic_plan_review_with_revise():
|
||||
return await super().replan(magentic_context)
|
||||
|
||||
manager = CountingManager()
|
||||
wf = (
|
||||
MagenticBuilder()
|
||||
.participants([DummyExec(name=manager.next_speaker_name)])
|
||||
.with_manager(manager=manager)
|
||||
.with_plan_review()
|
||||
.build()
|
||||
)
|
||||
wf = MagenticBuilder(
|
||||
participants=[DummyExec(name=manager.next_speaker_name)],
|
||||
enable_plan_review=True,
|
||||
manager=manager,
|
||||
).build()
|
||||
|
||||
# Wait for the initial plan review request
|
||||
req_event: WorkflowEvent | None = None
|
||||
@@ -324,12 +322,7 @@ async def test_magentic_plan_review_with_revise():
|
||||
|
||||
async def test_magentic_orchestrator_round_limit_produces_partial_result():
|
||||
manager = FakeManager(max_round_count=1)
|
||||
wf = (
|
||||
MagenticBuilder()
|
||||
.participants([DummyExec(name=manager.next_speaker_name)])
|
||||
.with_manager(manager=manager)
|
||||
.build()
|
||||
)
|
||||
wf = MagenticBuilder(participants=[DummyExec(name=manager.next_speaker_name)], manager=manager).build()
|
||||
|
||||
events: list[WorkflowEvent] = []
|
||||
async for ev in wf.run("round limit test", stream=True):
|
||||
@@ -354,14 +347,12 @@ async def test_magentic_checkpoint_resume_round_trip():
|
||||
storage = InMemoryCheckpointStorage()
|
||||
|
||||
manager1 = FakeManager()
|
||||
wf = (
|
||||
MagenticBuilder()
|
||||
.participants([DummyExec(name=manager1.next_speaker_name)])
|
||||
.with_manager(manager=manager1)
|
||||
.with_plan_review()
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
wf = MagenticBuilder(
|
||||
participants=[DummyExec(name=manager1.next_speaker_name)],
|
||||
enable_plan_review=True,
|
||||
checkpoint_storage=storage,
|
||||
manager=manager1,
|
||||
).build()
|
||||
|
||||
task_text = "checkpoint task"
|
||||
req_event: WorkflowEvent | None = None
|
||||
@@ -377,14 +368,12 @@ async def test_magentic_checkpoint_resume_round_trip():
|
||||
resume_checkpoint = checkpoints[-1]
|
||||
|
||||
manager2 = FakeManager()
|
||||
wf_resume = (
|
||||
MagenticBuilder()
|
||||
.participants([DummyExec(name=manager2.next_speaker_name)])
|
||||
.with_manager(manager=manager2)
|
||||
.with_plan_review()
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
wf_resume = MagenticBuilder(
|
||||
participants=[DummyExec(name=manager2.next_speaker_name)],
|
||||
enable_plan_review=True,
|
||||
checkpoint_storage=storage,
|
||||
manager=manager2,
|
||||
).build()
|
||||
|
||||
completed: WorkflowEvent | None = None
|
||||
req_event = None
|
||||
@@ -580,13 +569,7 @@ class StubAssistantsAgent(BaseAgent):
|
||||
async def _collect_agent_responses_setup(participant: SupportsAgentRun) -> list[ChatMessage]:
|
||||
captured: list[ChatMessage] = []
|
||||
|
||||
wf = (
|
||||
MagenticBuilder()
|
||||
.participants([participant])
|
||||
.with_manager(manager=InvokeOnceManager())
|
||||
.with_intermediate_outputs()
|
||||
.build()
|
||||
)
|
||||
wf = MagenticBuilder(participants=[participant], intermediate_outputs=True, manager=InvokeOnceManager()).build()
|
||||
|
||||
# Run a bounded stream to allow one invoke and then completion
|
||||
events: list[WorkflowEvent] = []
|
||||
@@ -632,13 +615,9 @@ async def _collect_checkpoints(
|
||||
async def test_magentic_checkpoint_resume_inner_loop_superstep():
|
||||
storage = InMemoryCheckpointStorage()
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants([StubThreadAgent()])
|
||||
.with_manager(manager=InvokeOnceManager())
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
workflow = MagenticBuilder(
|
||||
participants=[StubThreadAgent()], checkpoint_storage=storage, manager=InvokeOnceManager()
|
||||
).build()
|
||||
|
||||
async for event in workflow.run("inner-loop task", stream=True):
|
||||
if event.type == "output":
|
||||
@@ -647,13 +626,9 @@ async def test_magentic_checkpoint_resume_inner_loop_superstep():
|
||||
checkpoints = await _collect_checkpoints(storage)
|
||||
inner_loop_checkpoint = next(cp for cp in checkpoints if cp.metadata.get("superstep") == 1) # type: ignore[reportUnknownMemberType]
|
||||
|
||||
resumed = (
|
||||
MagenticBuilder()
|
||||
.participants([StubThreadAgent()])
|
||||
.with_manager(manager=InvokeOnceManager())
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
resumed = MagenticBuilder(
|
||||
participants=[StubThreadAgent()], checkpoint_storage=storage, manager=InvokeOnceManager()
|
||||
).build()
|
||||
|
||||
completed: WorkflowEvent | None = None
|
||||
async for event in resumed.run(checkpoint_id=inner_loop_checkpoint.checkpoint_id, stream=True): # type: ignore[reportUnknownMemberType]
|
||||
@@ -670,13 +645,7 @@ async def test_magentic_checkpoint_resume_from_saved_state():
|
||||
# Use the working InvokeOnceManager first to get a completed workflow
|
||||
manager = InvokeOnceManager()
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants([StubThreadAgent()])
|
||||
.with_manager(manager=manager)
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
workflow = MagenticBuilder(participants=[StubThreadAgent()], checkpoint_storage=storage, manager=manager).build()
|
||||
|
||||
async for event in workflow.run("checkpoint resume task", stream=True):
|
||||
if event.type == "output":
|
||||
@@ -687,13 +656,9 @@ async def test_magentic_checkpoint_resume_from_saved_state():
|
||||
# Verify we can resume from the last saved checkpoint
|
||||
resumed_state = checkpoints[-1] # Use the last checkpoint
|
||||
|
||||
resumed_workflow = (
|
||||
MagenticBuilder()
|
||||
.participants([StubThreadAgent()])
|
||||
.with_manager(manager=InvokeOnceManager())
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
resumed_workflow = MagenticBuilder(
|
||||
participants=[StubThreadAgent()], checkpoint_storage=storage, manager=InvokeOnceManager()
|
||||
).build()
|
||||
|
||||
completed: WorkflowEvent | None = None
|
||||
async for event in resumed_workflow.run(checkpoint_id=resumed_state.checkpoint_id, stream=True):
|
||||
@@ -708,14 +673,12 @@ async def test_magentic_checkpoint_resume_rejects_participant_renames():
|
||||
|
||||
manager = InvokeOnceManager()
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants([StubThreadAgent()])
|
||||
.with_manager(manager=manager)
|
||||
.with_plan_review()
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
workflow = MagenticBuilder(
|
||||
participants=[StubThreadAgent()],
|
||||
enable_plan_review=True,
|
||||
checkpoint_storage=storage,
|
||||
manager=manager,
|
||||
).build()
|
||||
|
||||
req_event: WorkflowEvent | None = None
|
||||
async for event in workflow.run("task", stream=True):
|
||||
@@ -728,14 +691,12 @@ async def test_magentic_checkpoint_resume_rejects_participant_renames():
|
||||
checkpoints = await _collect_checkpoints(storage)
|
||||
target_checkpoint = checkpoints[-1]
|
||||
|
||||
renamed_workflow = (
|
||||
MagenticBuilder()
|
||||
.participants([StubThreadAgent(name="renamedAgent")])
|
||||
.with_manager(manager=InvokeOnceManager())
|
||||
.with_plan_review()
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
renamed_workflow = MagenticBuilder(
|
||||
participants=[StubThreadAgent(name="renamedAgent")],
|
||||
enable_plan_review=True,
|
||||
checkpoint_storage=storage,
|
||||
manager=InvokeOnceManager(),
|
||||
).build()
|
||||
|
||||
with pytest.raises(WorkflowCheckpointException, match="Workflow graph has changed"):
|
||||
async for _ in renamed_workflow.run(
|
||||
@@ -772,7 +733,7 @@ class NotProgressingManager(MagenticManagerBase):
|
||||
async def test_magentic_stall_and_reset_reach_limits():
|
||||
manager = NotProgressingManager(max_round_count=10, max_stall_count=0, max_reset_count=1)
|
||||
|
||||
wf = MagenticBuilder().participants([DummyExec("agentA")]).with_manager(manager=manager).build()
|
||||
wf = MagenticBuilder(participants=[DummyExec("agentA")], manager=manager).build()
|
||||
|
||||
events: list[WorkflowEvent] = []
|
||||
async for ev in wf.run("test limits", stream=True):
|
||||
@@ -797,7 +758,7 @@ async def test_magentic_checkpoint_runtime_only() -> None:
|
||||
storage = InMemoryCheckpointStorage()
|
||||
|
||||
manager = FakeManager(max_round_count=10)
|
||||
wf = MagenticBuilder().participants([DummyExec("agentA")]).with_manager(manager=manager).build()
|
||||
wf = MagenticBuilder(participants=[DummyExec("agentA")], manager=manager).build()
|
||||
|
||||
baseline_output: ChatMessage | None = None
|
||||
async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True):
|
||||
@@ -829,13 +790,9 @@ async def test_magentic_checkpoint_runtime_overrides_buildtime() -> None:
|
||||
runtime_storage = FileCheckpointStorage(temp_dir2)
|
||||
|
||||
manager = FakeManager(max_round_count=10)
|
||||
wf = (
|
||||
MagenticBuilder()
|
||||
.participants([DummyExec("agentA")])
|
||||
.with_manager(manager=manager)
|
||||
.with_checkpointing(buildtime_storage)
|
||||
.build()
|
||||
)
|
||||
wf = MagenticBuilder(
|
||||
participants=[DummyExec("agentA")], checkpoint_storage=buildtime_storage, manager=manager
|
||||
).build()
|
||||
|
||||
baseline_output: ChatMessage | None = None
|
||||
async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True):
|
||||
@@ -884,13 +841,7 @@ async def test_magentic_checkpoint_restore_no_duplicate_history():
|
||||
manager = FakeManager(max_round_count=10)
|
||||
storage = InMemoryCheckpointStorage()
|
||||
|
||||
wf = (
|
||||
MagenticBuilder()
|
||||
.participants([DummyExec("agentA")])
|
||||
.with_manager(manager=manager)
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
wf = MagenticBuilder(participants=[DummyExec("agentA")], checkpoint_storage=storage, manager=manager).build()
|
||||
|
||||
# Run with conversation history to create initial checkpoint
|
||||
conversation: list[ChatMessage] = [
|
||||
@@ -947,47 +898,41 @@ async def test_magentic_checkpoint_restore_no_duplicate_history():
|
||||
def test_magentic_builder_rejects_empty_participant_factories():
|
||||
"""Test that MagenticBuilder rejects empty participant_factories list."""
|
||||
with pytest.raises(ValueError, match=r"participant_factories cannot be empty"):
|
||||
MagenticBuilder().register_participants([])
|
||||
MagenticBuilder(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\.",
|
||||
):
|
||||
MagenticBuilder().with_manager(manager=FakeManager()).build()
|
||||
MagenticBuilder()
|
||||
|
||||
|
||||
def test_magentic_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."""
|
||||
agent = StubAgent("agentA", "reply from agentA")
|
||||
|
||||
# Case 1: participants first, then register_participants
|
||||
with pytest.raises(ValueError, match="Cannot mix .participants"):
|
||||
MagenticBuilder().participants([agent]).register_participants([lambda: StubAgent("agentB", "reply")])
|
||||
|
||||
# Case 2: register_participants first, then participants
|
||||
with pytest.raises(ValueError, match="Cannot mix .participants"):
|
||||
MagenticBuilder().register_participants([lambda: agent]).participants([StubAgent("agentB", "reply")])
|
||||
|
||||
|
||||
def test_magentic_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."
|
||||
):
|
||||
(
|
||||
MagenticBuilder()
|
||||
.register_participants([lambda: StubAgent("agentA", "reply from agentA")])
|
||||
.register_participants([lambda: StubAgent("agentB", "reply from agentB")])
|
||||
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
|
||||
MagenticBuilder(
|
||||
participants=[agent],
|
||||
participant_factories=[lambda: StubAgent("agentB", "reply")],
|
||||
)
|
||||
|
||||
|
||||
def test_magentic_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"):
|
||||
(
|
||||
MagenticBuilder()
|
||||
.participants([StubAgent("agentA", "reply from agentA")])
|
||||
.participants([StubAgent("agentB", "reply from agentB")])
|
||||
def test_magentic_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"):
|
||||
MagenticBuilder(
|
||||
participant_factories=[lambda: StubAgent("agentA", "reply from agentA")],
|
||||
participants=[StubAgent("agentB", "reply from agentB")],
|
||||
)
|
||||
|
||||
|
||||
def test_magentic_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"):
|
||||
MagenticBuilder(
|
||||
participants=[StubAgent("agentA", "reply from agentA")],
|
||||
participant_factories=[lambda: StubAgent("agentB", "reply from agentB")],
|
||||
)
|
||||
|
||||
|
||||
@@ -1001,7 +946,7 @@ async def test_magentic_with_participant_factories():
|
||||
return StubAgent("agentA", "reply from agentA")
|
||||
|
||||
manager = FakeManager()
|
||||
workflow = MagenticBuilder().register_participants([create_agent]).with_manager(manager=manager).build()
|
||||
workflow = MagenticBuilder(participant_factories=[create_agent], manager=manager).build()
|
||||
|
||||
# Factory should be called during build
|
||||
assert call_count == 1
|
||||
@@ -1023,7 +968,7 @@ async def test_magentic_participant_factories_reusable_builder():
|
||||
call_count += 1
|
||||
return StubAgent("agentA", "reply from agentA")
|
||||
|
||||
builder = MagenticBuilder().register_participants([create_agent]).with_manager(manager=FakeManager())
|
||||
builder = MagenticBuilder(participant_factories=[create_agent], manager=FakeManager())
|
||||
|
||||
# Build first workflow
|
||||
wf1 = builder.build()
|
||||
@@ -1045,13 +990,9 @@ async def test_magentic_participant_factories_with_checkpointing():
|
||||
return StubAgent("agentA", "reply from agentA")
|
||||
|
||||
manager = FakeManager()
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.register_participants([create_agent])
|
||||
.with_manager(manager=manager)
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
workflow = MagenticBuilder(
|
||||
participant_factories=[create_agent], checkpoint_storage=storage, manager=manager
|
||||
).build()
|
||||
|
||||
outputs: list[WorkflowEvent] = []
|
||||
async for event in workflow.run("checkpoint test", stream=True):
|
||||
@@ -1072,27 +1013,27 @@ async def test_magentic_participant_factories_with_checkpointing():
|
||||
def test_magentic_builder_rejects_multiple_manager_configurations():
|
||||
"""Test that configuring multiple managers raises ValueError."""
|
||||
manager = FakeManager()
|
||||
agent = StubAgent("agentA", "reply")
|
||||
|
||||
builder = MagenticBuilder().with_manager(manager=manager)
|
||||
|
||||
with pytest.raises(ValueError, match=r"with_manager\(\) has already been called"):
|
||||
builder.with_manager(manager=manager)
|
||||
with pytest.raises(ValueError, match=r"Exactly one of"):
|
||||
MagenticBuilder(participants=[agent], manager=manager, manager_agent=StubManagerAgent())
|
||||
|
||||
|
||||
def test_magentic_builder_requires_exactly_one_manager_option():
|
||||
"""Test that exactly one manager option must be provided."""
|
||||
manager = FakeManager()
|
||||
agent = StubAgent("agentA", "reply")
|
||||
|
||||
def manager_factory() -> MagenticManagerBase:
|
||||
return FakeManager()
|
||||
|
||||
# No options provided
|
||||
with pytest.raises(ValueError, match="Exactly one of"):
|
||||
MagenticBuilder().with_manager() # type: ignore
|
||||
# No options provided - only fails at build() time
|
||||
with pytest.raises(ValueError, match="No manager configured"):
|
||||
MagenticBuilder(participants=[agent]).build()
|
||||
|
||||
# Multiple options provided
|
||||
with pytest.raises(ValueError, match="Exactly one of"):
|
||||
MagenticBuilder().with_manager(manager=manager, manager_factory=manager_factory) # type: ignore
|
||||
MagenticBuilder(participants=[agent], manager=manager, manager_factory=manager_factory)
|
||||
|
||||
|
||||
async def test_magentic_with_manager_factory():
|
||||
@@ -1105,7 +1046,7 @@ async def test_magentic_with_manager_factory():
|
||||
return FakeManager()
|
||||
|
||||
agent = StubAgent("agentA", "reply from agentA")
|
||||
workflow = MagenticBuilder().participants([agent]).with_manager(manager_factory=manager_factory).build()
|
||||
workflow = MagenticBuilder(participants=[agent], manager_factory=manager_factory).build()
|
||||
|
||||
# Factory should be called during build
|
||||
assert factory_call_count == 1
|
||||
@@ -1128,12 +1069,9 @@ async def test_magentic_with_agent_factory():
|
||||
return cast(SupportsAgentRun, StubManagerAgent())
|
||||
|
||||
participant = StubAgent("agentA", "reply from agentA")
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants([participant])
|
||||
.with_manager(agent_factory=agent_factory, max_round_count=1)
|
||||
.build()
|
||||
)
|
||||
workflow = MagenticBuilder(
|
||||
participants=[participant], manager_agent_factory=agent_factory, max_round_count=1
|
||||
).build()
|
||||
|
||||
# Factory should be called during build
|
||||
assert factory_call_count == 1
|
||||
@@ -1158,7 +1096,7 @@ async def test_magentic_manager_factory_reusable_builder():
|
||||
return FakeManager()
|
||||
|
||||
agent = StubAgent("agentA", "reply from agentA")
|
||||
builder = MagenticBuilder().participants([agent]).with_manager(manager_factory=manager_factory)
|
||||
builder = MagenticBuilder(participants=[agent], manager_factory=manager_factory)
|
||||
|
||||
# Build first workflow
|
||||
wf1 = builder.build()
|
||||
@@ -1189,9 +1127,7 @@ def test_magentic_with_both_participant_and_manager_factories():
|
||||
manager_factory_call_count += 1
|
||||
return FakeManager()
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder().register_participants([create_agent]).with_manager(manager_factory=manager_factory).build()
|
||||
)
|
||||
workflow = MagenticBuilder(participant_factories=[create_agent], manager_factory=manager_factory).build()
|
||||
|
||||
# All factories should be called during build
|
||||
assert participant_factory_call_count == 1
|
||||
@@ -1216,7 +1152,7 @@ async def test_magentic_factories_reusable_for_multiple_workflows():
|
||||
manager_factory_call_count += 1
|
||||
return FakeManager()
|
||||
|
||||
builder = MagenticBuilder().register_participants([create_agent]).with_manager(manager_factory=manager_factory)
|
||||
builder = MagenticBuilder(participant_factories=[create_agent], manager_factory=manager_factory)
|
||||
|
||||
# Build first workflow
|
||||
wf1 = builder.build()
|
||||
@@ -1266,25 +1202,21 @@ def test_magentic_agent_factory_with_standard_manager_options():
|
||||
)
|
||||
|
||||
participant = StubAgent("agentA", "reply from agentA")
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants([participant])
|
||||
.with_manager(
|
||||
agent_factory=agent_factory,
|
||||
task_ledger=custom_task_ledger,
|
||||
max_stall_count=custom_max_stall_count,
|
||||
max_reset_count=custom_max_reset_count,
|
||||
max_round_count=custom_max_round_count,
|
||||
task_ledger_facts_prompt=custom_facts_prompt,
|
||||
task_ledger_plan_prompt=custom_plan_prompt,
|
||||
task_ledger_full_prompt=custom_full_prompt,
|
||||
task_ledger_facts_update_prompt=custom_facts_update_prompt,
|
||||
task_ledger_plan_update_prompt=custom_plan_update_prompt,
|
||||
progress_ledger_prompt=custom_progress_prompt,
|
||||
final_answer_prompt=custom_final_prompt,
|
||||
)
|
||||
.build()
|
||||
)
|
||||
workflow = MagenticBuilder(
|
||||
participants=[participant],
|
||||
manager_agent_factory=agent_factory,
|
||||
task_ledger=custom_task_ledger,
|
||||
max_stall_count=custom_max_stall_count,
|
||||
max_reset_count=custom_max_reset_count,
|
||||
max_round_count=custom_max_round_count,
|
||||
task_ledger_facts_prompt=custom_facts_prompt,
|
||||
task_ledger_plan_prompt=custom_plan_prompt,
|
||||
task_ledger_full_prompt=custom_full_prompt,
|
||||
task_ledger_facts_update_prompt=custom_facts_update_prompt,
|
||||
task_ledger_plan_update_prompt=custom_plan_update_prompt,
|
||||
progress_ledger_prompt=custom_progress_prompt,
|
||||
final_answer_prompt=custom_final_prompt,
|
||||
).build()
|
||||
|
||||
# Factory should be called during build
|
||||
assert factory_call_count == 1
|
||||
|
||||
@@ -68,38 +68,36 @@ class _InvalidExecutor(Executor):
|
||||
|
||||
def test_sequential_builder_rejects_empty_participants() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
SequentialBuilder().participants([])
|
||||
SequentialBuilder(participants=[])
|
||||
|
||||
|
||||
def test_sequential_builder_rejects_empty_participant_factories() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
SequentialBuilder().register_participants([])
|
||||
SequentialBuilder(participant_factories=[])
|
||||
|
||||
|
||||
def test_sequential_builder_rejects_mixing_participants_and_factories() -> None:
|
||||
"""Test that mixing .participants() and .register_participants() raises an error."""
|
||||
"""Test that passing both participants and participant_factories to the constructor raises an error."""
|
||||
a1 = _EchoAgent(id="agent1", name="A1")
|
||||
|
||||
# Try .participants() then .register_participants()
|
||||
with pytest.raises(ValueError, match="Cannot mix"):
|
||||
SequentialBuilder().participants([a1]).register_participants([lambda: _EchoAgent(id="agent2", name="A2")])
|
||||
|
||||
# Try .register_participants() then .participants()
|
||||
with pytest.raises(ValueError, match="Cannot mix"):
|
||||
SequentialBuilder().register_participants([lambda: _EchoAgent(id="agent1", name="A1")]).participants([a1])
|
||||
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
|
||||
SequentialBuilder(
|
||||
participants=[a1],
|
||||
participant_factories=[lambda: _EchoAgent(id="agent2", name="A2")],
|
||||
)
|
||||
|
||||
|
||||
def test_sequential_builder_validation_rejects_invalid_executor() -> None:
|
||||
"""Test that adding an invalid executor to the builder raises an error."""
|
||||
with pytest.raises(TypeCompatibilityError):
|
||||
SequentialBuilder().participants([_EchoAgent(id="agent1", name="A1"), _InvalidExecutor(id="invalid")]).build()
|
||||
SequentialBuilder(participants=[_EchoAgent(id="agent1", name="A1"), _InvalidExecutor(id="invalid")]).build()
|
||||
|
||||
|
||||
async def test_sequential_agents_append_to_context() -> None:
|
||||
a1 = _EchoAgent(id="agent1", name="A1")
|
||||
a2 = _EchoAgent(id="agent2", name="A2")
|
||||
|
||||
wf = SequentialBuilder().participants([a1, a2]).build()
|
||||
wf = SequentialBuilder(participants=[a1, a2]).build()
|
||||
|
||||
completed = False
|
||||
output: list[ChatMessage] | None = None
|
||||
@@ -132,7 +130,7 @@ async def test_sequential_register_participants_with_agent_factories() -> None:
|
||||
def create_agent2() -> _EchoAgent:
|
||||
return _EchoAgent(id="agent2", name="A2")
|
||||
|
||||
wf = SequentialBuilder().register_participants([create_agent1, create_agent2]).build()
|
||||
wf = SequentialBuilder(participant_factories=[create_agent1, create_agent2]).build()
|
||||
|
||||
completed = False
|
||||
output: list[ChatMessage] | None = None
|
||||
@@ -158,7 +156,7 @@ async def test_sequential_with_custom_executor_summary() -> None:
|
||||
a1 = _EchoAgent(id="agent1", name="A1")
|
||||
summarizer = _SummarizerExec(id="summarizer")
|
||||
|
||||
wf = SequentialBuilder().participants([a1, summarizer]).build()
|
||||
wf = SequentialBuilder(participants=[a1, summarizer]).build()
|
||||
|
||||
completed = False
|
||||
output: list[ChatMessage] | None = None
|
||||
@@ -189,7 +187,7 @@ async def test_sequential_register_participants_mixed_agents_and_executors() ->
|
||||
def create_summarizer() -> _SummarizerExec:
|
||||
return _SummarizerExec(id="summarizer")
|
||||
|
||||
wf = SequentialBuilder().register_participants([create_agent, create_summarizer]).build()
|
||||
wf = SequentialBuilder(participant_factories=[create_agent, create_summarizer]).build()
|
||||
|
||||
completed = False
|
||||
output: list[ChatMessage] | None = None
|
||||
@@ -215,7 +213,7 @@ async def test_sequential_checkpoint_resume_round_trip() -> None:
|
||||
storage = InMemoryCheckpointStorage()
|
||||
|
||||
initial_agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
|
||||
wf = SequentialBuilder().participants(list(initial_agents)).with_checkpointing(storage).build()
|
||||
wf = SequentialBuilder(participants=list(initial_agents), checkpoint_storage=storage).build()
|
||||
|
||||
baseline_output: list[ChatMessage] | None = None
|
||||
async for ev in wf.run("checkpoint sequential", stream=True):
|
||||
@@ -236,7 +234,7 @@ async def test_sequential_checkpoint_resume_round_trip() -> None:
|
||||
)
|
||||
|
||||
resumed_agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
|
||||
wf_resume = SequentialBuilder().participants(list(resumed_agents)).with_checkpointing(storage).build()
|
||||
wf_resume = SequentialBuilder(participants=list(resumed_agents), checkpoint_storage=storage).build()
|
||||
|
||||
resumed_output: list[ChatMessage] | None = None
|
||||
async for ev in wf_resume.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
|
||||
@@ -258,7 +256,7 @@ async def test_sequential_checkpoint_runtime_only() -> None:
|
||||
storage = InMemoryCheckpointStorage()
|
||||
|
||||
agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
|
||||
wf = SequentialBuilder().participants(list(agents)).build()
|
||||
wf = SequentialBuilder(participants=list(agents)).build()
|
||||
|
||||
baseline_output: list[ChatMessage] | None = None
|
||||
async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True):
|
||||
@@ -279,7 +277,7 @@ async def test_sequential_checkpoint_runtime_only() -> None:
|
||||
)
|
||||
|
||||
resumed_agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
|
||||
wf_resume = SequentialBuilder().participants(list(resumed_agents)).build()
|
||||
wf_resume = SequentialBuilder(participants=list(resumed_agents)).build()
|
||||
|
||||
resumed_output: list[ChatMessage] | None = None
|
||||
async for ev in wf_resume.run(
|
||||
@@ -309,7 +307,7 @@ async def test_sequential_checkpoint_runtime_overrides_buildtime() -> None:
|
||||
runtime_storage = FileCheckpointStorage(temp_dir2)
|
||||
|
||||
agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
|
||||
wf = SequentialBuilder().participants(list(agents)).with_checkpointing(buildtime_storage).build()
|
||||
wf = SequentialBuilder(participants=list(agents), checkpoint_storage=buildtime_storage).build()
|
||||
|
||||
baseline_output: list[ChatMessage] | None = None
|
||||
async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True):
|
||||
@@ -337,7 +335,7 @@ async def test_sequential_register_participants_with_checkpointing() -> None:
|
||||
def create_agent2() -> _EchoAgent:
|
||||
return _EchoAgent(id="agent2", name="A2")
|
||||
|
||||
wf = SequentialBuilder().register_participants([create_agent1, create_agent2]).with_checkpointing(storage).build()
|
||||
wf = SequentialBuilder(participant_factories=[create_agent1, create_agent2], checkpoint_storage=storage).build()
|
||||
|
||||
baseline_output: list[ChatMessage] | None = None
|
||||
async for ev in wf.run("checkpoint with factories", stream=True):
|
||||
@@ -357,9 +355,9 @@ async def test_sequential_register_participants_with_checkpointing() -> None:
|
||||
checkpoints[-1],
|
||||
)
|
||||
|
||||
wf_resume = (
|
||||
SequentialBuilder().register_participants([create_agent1, create_agent2]).with_checkpointing(storage).build()
|
||||
)
|
||||
wf_resume = SequentialBuilder(
|
||||
participant_factories=[create_agent1, create_agent2], checkpoint_storage=storage
|
||||
).build()
|
||||
|
||||
resumed_output: list[ChatMessage] | None = None
|
||||
async for ev in wf_resume.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
|
||||
@@ -385,7 +383,7 @@ async def test_sequential_register_participants_factories_called_on_build() -> N
|
||||
call_count += 1
|
||||
return _EchoAgent(id=f"agent{call_count}", name=f"A{call_count}")
|
||||
|
||||
builder = SequentialBuilder().register_participants([create_agent, create_agent])
|
||||
builder = SequentialBuilder(participant_factories=[create_agent, create_agent])
|
||||
|
||||
# Factories should not be called yet
|
||||
assert call_count == 0
|
||||
@@ -418,7 +416,7 @@ async def test_sequential_builder_reusable_after_build_with_participants() -> No
|
||||
a1 = _EchoAgent(id="agent1", name="A1")
|
||||
a2 = _EchoAgent(id="agent2", name="A2")
|
||||
|
||||
builder = SequentialBuilder().participants([a1, a2])
|
||||
builder = SequentialBuilder(participants=[a1, a2])
|
||||
|
||||
# Build first workflow
|
||||
builder.build()
|
||||
@@ -442,7 +440,7 @@ async def test_sequential_builder_reusable_after_build_with_factories() -> None:
|
||||
call_count += 1
|
||||
return _EchoAgent(id="agent2", name="A2")
|
||||
|
||||
builder = SequentialBuilder().register_participants([create_agent1, create_agent2])
|
||||
builder = SequentialBuilder(participant_factories=[create_agent1, create_agent2])
|
||||
|
||||
# Build first workflow - factories should be called
|
||||
builder.build()
|
||||
|
||||
Reference in New Issue
Block a user