[BREAKING] Python: Add factory pattern to GroupChat and Magentic (#3224)

* group chat

* magentic

* Fix tests

* AI comments

* Unifiy error message and add warning

* misc

* Add overload

* Collapse orchestrator params
This commit is contained in:
Tao Chen
2026-01-28 09:00:20 -08:00
committed by GitHub
Unverified
parent a7d924a7d2
commit 739edc7307
24 changed files with 1488 additions and 365 deletions
@@ -1,6 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncIterable, Callable
from collections.abc import AsyncIterable, Callable, Sequence
from typing import Any, cast
import pytest
@@ -40,7 +40,7 @@ class StubAgent(BaseAgent):
async def run( # type: ignore[override]
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
@@ -50,7 +50,7 @@ class StubAgent(BaseAgent):
def run_stream( # type: ignore[override]
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
@@ -66,9 +66,7 @@ class StubAgent(BaseAgent):
class MockChatClient:
"""Mock chat client that raises NotImplementedError for all methods."""
@property
def additional_properties(self) -> dict[str, Any]:
return {}
additional_properties: dict[str, Any]
async def get_response(self, messages: Any, **kwargs: Any) -> ChatResponse:
raise NotImplementedError
@@ -84,7 +82,7 @@ class StubManagerAgent(ChatAgent):
async def run(
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
@@ -130,7 +128,7 @@ class StubManagerAgent(ChatAgent):
def run_stream(
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
@@ -230,7 +228,7 @@ async def test_group_chat_builder_basic_flow() -> None:
workflow = (
GroupChatBuilder()
.with_select_speaker_func(selector, orchestrator_name="manager")
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha, beta])
.with_max_rounds(2) # Limit rounds to prevent infinite loop
.build()
@@ -257,7 +255,7 @@ async def test_group_chat_as_agent_accepts_conversation() -> None:
workflow = (
GroupChatBuilder()
.with_select_speaker_func(selector, orchestrator_name="manager")
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha, beta])
.with_max_rounds(2) # Limit rounds to prevent infinite loop
.build()
@@ -285,7 +283,9 @@ class TestGroupChatBuilder:
builder = GroupChatBuilder().participants([agent])
with pytest.raises(RuntimeError, match="Orchestrator could not be resolved"):
with pytest.raises(
ValueError, match=r"No orchestrator has been configured\. Call with_orchestrator\(\) to set one\."
):
builder.build()
def test_build_without_participants_raises_error(self) -> None:
@@ -294,9 +294,12 @@ class TestGroupChatBuilder:
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_select_speaker_func(selector)
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="participants must be configured before build"):
with pytest.raises(
ValueError,
match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first\.",
):
builder.build()
def test_duplicate_manager_configuration_raises_error(self) -> None:
@@ -305,10 +308,13 @@ class TestGroupChatBuilder:
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_select_speaker_func(selector)
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="select_speakers_func has already been configured"):
builder.with_select_speaker_func(selector)
with pytest.raises(
ValueError,
match=r"A selection function has already been configured\. Call with_orchestrator\(\.\.\.\) once only\.",
):
builder.with_orchestrator(selection_func=selector)
def test_empty_participants_raises_error(self) -> None:
"""Test that empty participants list raises ValueError."""
@@ -316,7 +322,7 @@ class TestGroupChatBuilder:
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_select_speaker_func(selector)
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="participants cannot be empty"):
builder.participants([])
@@ -329,7 +335,7 @@ class TestGroupChatBuilder:
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_select_speaker_func(selector)
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="Duplicate participant name 'test'"):
builder.participants([agent1, agent2])
@@ -357,7 +363,7 @@ class TestGroupChatBuilder:
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_select_speaker_func(selector)
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="AgentProtocol participants must have a non-empty name"):
builder.participants([agent])
@@ -369,7 +375,7 @@ class TestGroupChatBuilder:
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_select_speaker_func(selector)
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="AgentProtocol participants must have a non-empty name"):
builder.participants([agent])
@@ -391,7 +397,7 @@ class TestGroupChatWorkflow:
workflow = (
GroupChatBuilder()
.with_select_speaker_func(selector)
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(2) # Limit to 2 rounds
.build()
@@ -426,7 +432,7 @@ class TestGroupChatWorkflow:
workflow = (
GroupChatBuilder()
.with_select_speaker_func(selector)
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_termination_condition(termination_condition)
.build()
@@ -454,7 +460,7 @@ class TestGroupChatWorkflow:
workflow = (
GroupChatBuilder()
.with_agent_orchestrator(manager)
.with_orchestrator(agent=manager)
.participants([worker])
.with_termination_condition(lambda conv: any(msg.author_name == "agent" for msg in conv))
.build()
@@ -480,7 +486,7 @@ class TestGroupChatWorkflow:
agent = StubAgent("agent", "response")
workflow = GroupChatBuilder().with_select_speaker_func(selector).participants([agent]).build()
workflow = GroupChatBuilder().with_orchestrator(selection_func=selector).participants([agent]).build()
with pytest.raises(RuntimeError, match="Selection function returned unknown participant 'unknown_agent'"):
async for _ in workflow.run_stream("test task"):
@@ -501,7 +507,7 @@ class TestCheckpointing:
workflow = (
GroupChatBuilder()
.with_select_speaker_func(selector)
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.with_checkpointing(storage)
@@ -530,7 +536,11 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder().with_select_speaker_func(selector).participants([agent]).with_max_rounds(1).build()
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
with pytest.raises(ValueError, match="At least one ChatMessage is required to start the group chat workflow."):
@@ -550,7 +560,11 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder().with_select_speaker_func(selector).participants([agent]).with_max_rounds(1).build()
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
outputs: list[list[ChatMessage]] = []
@@ -575,7 +589,11 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder().with_select_speaker_func(selector).participants([agent]).with_max_rounds(1).build()
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
outputs: list[list[ChatMessage]] = []
@@ -603,7 +621,11 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder().with_select_speaker_func(selector).participants([agent]).with_max_rounds(1).build()
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
outputs: list[list[ChatMessage]] = []
@@ -632,7 +654,7 @@ class TestRoundLimitEnforcement:
workflow = (
GroupChatBuilder()
.with_select_speaker_func(selector)
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1) # Very low limit
.build()
@@ -667,7 +689,7 @@ class TestRoundLimitEnforcement:
workflow = (
GroupChatBuilder()
.with_select_speaker_func(selector)
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1) # Hit limit after first response
.build()
@@ -700,7 +722,7 @@ async def test_group_chat_checkpoint_runtime_only() -> None:
wf = (
GroupChatBuilder()
.participants([agent_a, agent_b])
.with_select_speaker_func(selector)
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
.build()
)
@@ -738,7 +760,7 @@ async def test_group_chat_checkpoint_runtime_overrides_buildtime() -> None:
wf = (
GroupChatBuilder()
.participants([agent_a, agent_b])
.with_select_speaker_func(selector)
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
.with_checkpointing(buildtime_storage)
.build()
@@ -783,7 +805,7 @@ async def test_group_chat_with_request_info_filtering():
workflow = (
GroupChatBuilder()
.with_select_speaker_func(selector, orchestrator_name="manager")
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha, beta])
.with_max_rounds(2)
.with_request_info(agents=["beta"]) # Only pause before beta runs
@@ -835,7 +857,7 @@ async def test_group_chat_with_request_info_no_filter_pauses_all():
workflow = (
GroupChatBuilder()
.with_select_speaker_func(selector, orchestrator_name="manager")
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha])
.with_max_rounds(1)
.with_request_info() # No filter - pause for all
@@ -864,3 +886,450 @@ def test_group_chat_builder_with_request_info_returns_self():
builder2 = GroupChatBuilder()
result2 = builder2.with_request_info(agents=["test"])
assert result2 is builder2
# region Participant Factory Tests
def test_group_chat_builder_rejects_empty_participant_factories():
"""Test that GroupChatBuilder rejects empty participant_factories list."""
def selector(state: GroupChatState) -> str:
return list(state.participants.keys())[0]
with pytest.raises(ValueError, match=r"participant_factories cannot be empty"):
GroupChatBuilder().register_participants([])
with pytest.raises(
ValueError,
match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first\.",
):
GroupChatBuilder().with_orchestrator(selection_func=selector).build()
def test_group_chat_builder_rejects_mixing_participants_and_factories():
"""Test that mixing .participants() and .register_participants() 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")])
)
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")])
)
async def test_group_chat_with_participant_factories():
"""Test workflow creation using participant_factories."""
call_count = 0
def create_alpha() -> StubAgent:
nonlocal call_count
call_count += 1
return StubAgent("alpha", "reply from alpha")
def create_beta() -> StubAgent:
nonlocal call_count
call_count += 1
return StubAgent("beta", "reply from beta")
selector = make_sequence_selector()
workflow = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
.build()
)
# Factories should be called during build
assert call_count == 2
outputs: list[WorkflowOutputEvent] = []
async for event in workflow.run_stream("coordinate task"):
if isinstance(event, WorkflowOutputEvent):
outputs.append(event)
assert len(outputs) == 1
async def test_group_chat_participant_factories_reusable_builder():
"""Test that the builder can be reused to build multiple workflows with factories."""
call_count = 0
def create_alpha() -> StubAgent:
nonlocal call_count
call_count += 1
return StubAgent("alpha", "reply from alpha")
def create_beta() -> StubAgent:
nonlocal call_count
call_count += 1
return StubAgent("beta", "reply from beta")
selector = make_sequence_selector()
builder = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
)
# Build first workflow
wf1 = builder.build()
assert call_count == 2
# Build second workflow
wf2 = builder.build()
assert call_count == 4
# Verify that the two workflows have different agent instances
assert wf1.executors["alpha"] is not wf2.executors["alpha"]
assert wf1.executors["beta"] is not wf2.executors["beta"]
async def test_group_chat_participant_factories_with_checkpointing():
"""Test checkpointing with participant_factories."""
storage = InMemoryCheckpointStorage()
def create_alpha() -> StubAgent:
return StubAgent("alpha", "reply from alpha")
def create_beta() -> StubAgent:
return StubAgent("beta", "reply from beta")
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()
)
outputs: list[WorkflowOutputEvent] = []
async for event in workflow.run_stream("checkpoint test"):
if isinstance(event, WorkflowOutputEvent):
outputs.append(event)
assert outputs, "Should have workflow output"
checkpoints = await storage.list_checkpoints()
assert checkpoints, "Checkpoints should be created during workflow execution"
# endregion
# region Orchestrator Factory Tests
def test_group_chat_builder_rejects_multiple_orchestrator_configurations():
"""Test that configuring multiple orchestrators raises ValueError."""
def selector(state: GroupChatState) -> str:
return list(state.participants.keys())[0]
def agent_factory() -> ChatAgent:
return cast(ChatAgent, StubManagerAgent())
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
# 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)
# 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)
def test_group_chat_builder_requires_exactly_one_orchestrator_option():
"""Test that exactly one orchestrator option must be provided."""
def selector(state: GroupChatState) -> str:
return list(state.participants.keys())[0]
def agent_factory() -> ChatAgent:
return cast(ChatAgent, StubManagerAgent())
# No options provided
with pytest.raises(ValueError, match="Exactly one of"):
GroupChatBuilder().with_orchestrator() # type: ignore
# Multiple options provided
with pytest.raises(ValueError, match="Exactly one of"):
GroupChatBuilder().with_orchestrator(selection_func=selector, agent=agent_factory) # type: ignore
async def test_group_chat_with_orchestrator_factory_returning_chat_agent():
"""Test workflow creation using orchestrator_factory that returns ChatAgent."""
factory_call_count = 0
class DynamicManagerAgent(ChatAgent):
"""Manager agent that dynamically selects from available participants."""
def __init__(self) -> None:
super().__init__(chat_client=MockChatClient(), name="dynamic_manager", description="Dynamic manager")
self._call_count = 0
async def run(
self,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
) -> AgentResponse:
if self._call_count == 0:
self._call_count += 1
payload = {
"terminate": False,
"reason": "Selecting alpha",
"next_speaker": "alpha",
"final_message": None,
}
return AgentResponse(
messages=[
ChatMessage(
role=Role.ASSISTANT,
text=(
'{"terminate": false, "reason": "Selecting alpha", '
'"next_speaker": "alpha", "final_message": null}'
),
author_name=self.name,
)
],
value=payload,
)
payload = {
"terminate": True,
"reason": "Task complete",
"next_speaker": None,
"final_message": "dynamic manager final",
}
return AgentResponse(
messages=[
ChatMessage(
role=Role.ASSISTANT,
text=(
'{"terminate": true, "reason": "Task complete", '
'"next_speaker": null, "final_message": "dynamic manager final"}'
),
author_name=self.name,
)
],
value=payload,
)
def agent_factory() -> ChatAgent:
nonlocal factory_call_count
factory_call_count += 1
return cast(ChatAgent, DynamicManagerAgent())
alpha = StubAgent("alpha", "reply from alpha")
beta = StubAgent("beta", "reply from beta")
workflow = GroupChatBuilder().participants([alpha, beta]).with_orchestrator(agent=agent_factory).build()
# Factory should be called during build
assert factory_call_count == 1
outputs: list[WorkflowOutputEvent] = []
async for event in workflow.run_stream("coordinate task"):
if isinstance(event, WorkflowOutputEvent):
outputs.append(event)
assert len(outputs) == 1
# The DynamicManagerAgent terminates after second call with final_message
final_messages = outputs[0].data
assert isinstance(final_messages, list)
assert any(
msg.text == "dynamic manager final"
for msg in cast(list[ChatMessage], final_messages)
if msg.author_name == "dynamic_manager"
)
def test_group_chat_with_orchestrator_factory_returning_base_orchestrator():
"""Test that orchestrator_factory returning BaseGroupChatOrchestrator is used as-is."""
factory_call_count = 0
selector = make_sequence_selector()
def orchestrator_factory() -> BaseGroupChatOrchestrator:
nonlocal factory_call_count
factory_call_count += 1
from agent_framework._workflows._base_group_chat_orchestrator import ParticipantRegistry
from agent_framework._workflows._group_chat import GroupChatOrchestrator
# Create a custom orchestrator; when returning BaseGroupChatOrchestrator,
# the builder uses it as-is without modifying its participant registry
return GroupChatOrchestrator(
id="custom_orchestrator",
participant_registry=ParticipantRegistry([]),
selection_func=selector,
max_rounds=2,
)
alpha = StubAgent("alpha", "reply from alpha")
workflow = GroupChatBuilder().participants([alpha]).with_orchestrator(orchestrator=orchestrator_factory).build()
# Factory should be called during build
assert factory_call_count == 1
# Verify the custom orchestrator is in the workflow
assert "custom_orchestrator" in workflow.executors
async def test_group_chat_orchestrator_factory_reusable_builder():
"""Test that the builder can be reused to build multiple workflows with orchestrator factory."""
factory_call_count = 0
def agent_factory() -> ChatAgent:
nonlocal factory_call_count
factory_call_count += 1
return cast(ChatAgent, StubManagerAgent())
alpha = StubAgent("alpha", "reply from alpha")
beta = StubAgent("beta", "reply from beta")
builder = GroupChatBuilder().participants([alpha, beta]).with_orchestrator(agent=agent_factory)
# Build first workflow
wf1 = builder.build()
assert factory_call_count == 1
# Build second workflow
wf2 = builder.build()
assert factory_call_count == 2
# Verify that the two workflows have different orchestrator instances
assert wf1.executors["manager_agent"] is not wf2.executors["manager_agent"]
def test_group_chat_orchestrator_factory_invalid_return_type():
"""Test that orchestrator_factory raising error for invalid return type."""
def invalid_factory() -> Any:
return "invalid type"
alpha = StubAgent("alpha", "reply from alpha")
with pytest.raises(
TypeError,
match=r"Orchestrator factory must return ChatAgent or BaseGroupChatOrchestrator instance",
):
(GroupChatBuilder().participants([alpha]).with_orchestrator(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())
def test_group_chat_with_both_participant_and_orchestrator_factories():
"""Test workflow creation using both participant_factories and orchestrator_factory."""
participant_factory_call_count = 0
agent_factory_call_count = 0
def create_alpha() -> StubAgent:
nonlocal participant_factory_call_count
participant_factory_call_count += 1
return StubAgent("alpha", "reply from alpha")
def create_beta() -> StubAgent:
nonlocal participant_factory_call_count
participant_factory_call_count += 1
return StubAgent("beta", "reply from beta")
def agent_factory() -> ChatAgent:
nonlocal agent_factory_call_count
agent_factory_call_count += 1
return cast(ChatAgent, StubManagerAgent())
workflow = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(agent=agent_factory)
.build()
)
# All factories should be called during build
assert participant_factory_call_count == 2
assert agent_factory_call_count == 1
# Verify all executors are present in the workflow
assert "alpha" in workflow.executors
assert "beta" in workflow.executors
assert "manager_agent" in workflow.executors
async def test_group_chat_factories_reusable_for_multiple_workflows():
"""Test that both factories are reused correctly for multiple workflow builds."""
participant_factory_call_count = 0
agent_factory_call_count = 0
def create_alpha() -> StubAgent:
nonlocal participant_factory_call_count
participant_factory_call_count += 1
return StubAgent("alpha", "reply from alpha")
def create_beta() -> StubAgent:
nonlocal participant_factory_call_count
participant_factory_call_count += 1
return StubAgent("beta", "reply from beta")
def agent_factory() -> ChatAgent:
nonlocal agent_factory_call_count
agent_factory_call_count += 1
return cast(ChatAgent, StubManagerAgent())
builder = (
GroupChatBuilder().register_participants([create_alpha, create_beta]).with_orchestrator(agent=agent_factory)
)
# Build first workflow
wf1 = builder.build()
assert participant_factory_call_count == 2
assert agent_factory_call_count == 1
# Build second workflow
wf2 = builder.build()
assert participant_factory_call_count == 4
assert agent_factory_call_count == 2
# Verify that the workflows have different agent and orchestrator instances
assert wf1.executors["alpha"] is not wf2.executors["alpha"]
assert wf1.executors["beta"] is not wf2.executors["beta"]
assert wf1.executors["manager_agent"] is not wf2.executors["manager_agent"]
# endregion
@@ -209,7 +209,9 @@ def test_build_fails_without_start_agent():
def test_build_fails_without_participants():
"""Verify that build() raises ValueError when no participants are provided."""
with pytest.raises(ValueError, match="No participants or participant_factories have been configured."):
with pytest.raises(
ValueError, match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first."
):
HandoffBuilder().build()
@@ -273,7 +275,7 @@ async def test_tool_choice_preserved_from_agent_config():
agent = ChatAgent(
chat_client=mock_client,
name="test_agent",
default_options={"tool_choice": {"mode": "required"}},
default_options={"tool_choice": {"mode": "required"}}, # type: ignore
)
# Run the agent
@@ -293,9 +295,11 @@ def test_handoff_builder_rejects_empty_participant_factories():
"""Test that HandoffBuilder rejects empty participant_factories dictionary."""
# Empty factories are rejected immediately when calling participant_factories()
with pytest.raises(ValueError, match=r"participant_factories cannot be empty"):
HandoffBuilder().participant_factories({})
HandoffBuilder().register_participants({})
with pytest.raises(ValueError, match=r"No participants or participant_factories have been configured"):
with pytest.raises(
ValueError, match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first\."
):
HandoffBuilder(participant_factories={}).build()
@@ -312,7 +316,7 @@ def test_handoff_builder_rejects_mixing_participants_and_participant_factories_m
# Case 1: participants first, then participant_factories
with pytest.raises(ValueError, match="Cannot mix .participants"):
HandoffBuilder(participants=[triage]).participant_factories({
HandoffBuilder(participants=[triage]).register_participants({
"specialist": lambda: MockHandoffAgent(name="specialist")
})
@@ -324,13 +328,13 @@ def test_handoff_builder_rejects_mixing_participants_and_participant_factories_m
# Case 3: participants(), then participant_factories()
with pytest.raises(ValueError, match="Cannot mix .participants"):
HandoffBuilder().participants([triage]).participant_factories({
HandoffBuilder().participants([triage]).register_participants({
"specialist": lambda: MockHandoffAgent(name="specialist")
})
# Case 4: participant_factories(), then participants()
with pytest.raises(ValueError, match="Cannot mix .participants"):
HandoffBuilder().participant_factories({"triage": lambda: triage}).participants([
HandoffBuilder().register_participants({"triage": lambda: triage}).participants([
MockHandoffAgent(name="specialist")
])
@@ -343,11 +347,13 @@ def test_handoff_builder_rejects_mixing_participants_and_participant_factories_m
def test_handoff_builder_rejects_multiple_calls_to_participant_factories():
"""Test that multiple calls to .participant_factories() raises an error."""
with pytest.raises(ValueError, match=r"participant_factories\(\) has already been called"):
with pytest.raises(
ValueError, match=r"register_participants\(\) has already been called on this builder instance."
):
(
HandoffBuilder()
.participant_factories({"agent1": lambda: MockHandoffAgent(name="agent1")})
.participant_factories({"agent2": lambda: MockHandoffAgent(name="agent2")})
.register_participants({"agent1": lambda: MockHandoffAgent(name="agent1")})
.register_participants({"agent2": lambda: MockHandoffAgent(name="agent2")})
)
@@ -386,7 +392,7 @@ def test_handoff_builder_rejects_factory_name_coordinator_with_instances():
triage = MockHandoffAgent(name="triage")
specialist = MockHandoffAgent(name="specialist")
with pytest.raises(ValueError, match="Call participant_factories.*before with_start_agent"):
with pytest.raises(ValueError, match=r"Call register_participants\(...\) before with_start_agent\(...\)"):
(
HandoffBuilder(participants=[triage, specialist]).with_start_agent(
"triage"
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from collections.abc import AsyncIterable
from collections.abc import AsyncIterable, Sequence
from dataclasses import dataclass
from typing import Any, ClassVar, cast
@@ -155,7 +155,7 @@ class StubAgent(BaseAgent):
async def run( # type: ignore[override]
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
@@ -165,7 +165,7 @@ class StubAgent(BaseAgent):
def run_stream( # type: ignore[override]
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
@@ -193,7 +193,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_standard_manager(manager).build()
workflow = MagenticBuilder().participants([agent]).with_manager(manager=manager).build()
assert isinstance(workflow, Workflow)
@@ -219,7 +219,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_standard_manager(manager).build()
workflow = MagenticBuilder().participants([writer]).with_manager(manager=manager).build()
agent = workflow.as_agent(name="magentic-agent")
conversation = [
@@ -247,7 +247,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_standard_manager(manager).with_plan_review().build()
wf = MagenticBuilder().participants([DummyExec("agentA")]).with_manager(manager=manager).with_plan_review().build()
req_event: RequestInfoEvent | None = None
async for ev in wf.run_stream("do work"):
@@ -288,7 +288,7 @@ async def test_magentic_plan_review_with_revise():
wf = (
MagenticBuilder()
.participants([DummyExec(name=manager.next_speaker_name)])
.with_standard_manager(manager)
.with_manager(manager=manager)
.with_plan_review()
.build()
)
@@ -333,7 +333,7 @@ async def test_magentic_orchestrator_round_limit_produces_partial_result():
wf = (
MagenticBuilder()
.participants([DummyExec(name=manager.next_speaker_name)])
.with_standard_manager(manager)
.with_manager(manager=manager)
.build()
)
@@ -363,7 +363,7 @@ async def test_magentic_checkpoint_resume_round_trip():
wf = (
MagenticBuilder()
.participants([DummyExec(name=manager1.next_speaker_name)])
.with_standard_manager(manager1)
.with_manager(manager=manager1)
.with_plan_review()
.with_checkpointing(storage)
.build()
@@ -386,7 +386,7 @@ async def test_magentic_checkpoint_resume_round_trip():
wf_resume = (
MagenticBuilder()
.participants([DummyExec(name=manager2.next_speaker_name)])
.with_standard_manager(manager2)
.with_manager(manager=manager2)
.with_plan_review()
.with_checkpointing(storage)
.build()
@@ -422,7 +422,7 @@ class StubManagerAgent(BaseAgent):
async def run(
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: Any = None,
**kwargs: Any,
@@ -431,7 +431,7 @@ class StubManagerAgent(BaseAgent):
def run_stream(
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: Any = None,
**kwargs: Any,
@@ -575,7 +575,7 @@ class StubAssistantsAgent(BaseAgent):
async def _collect_agent_responses_setup(participant: AgentProtocol) -> list[ChatMessage]:
captured: list[ChatMessage] = []
wf = MagenticBuilder().participants([participant]).with_standard_manager(InvokeOnceManager()).build()
wf = MagenticBuilder().participants([participant]).with_manager(manager=InvokeOnceManager()).build()
# Run a bounded stream to allow one invoke and then completion
events: list[WorkflowEvent] = []
@@ -623,7 +623,7 @@ async def test_magentic_checkpoint_resume_inner_loop_superstep():
workflow = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_standard_manager(InvokeOnceManager())
.with_manager(manager=InvokeOnceManager())
.with_checkpointing(storage)
.build()
)
@@ -638,7 +638,7 @@ async def test_magentic_checkpoint_resume_inner_loop_superstep():
resumed = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_standard_manager(InvokeOnceManager())
.with_manager(manager=InvokeOnceManager())
.with_checkpointing(storage)
.build()
)
@@ -661,7 +661,7 @@ async def test_magentic_checkpoint_resume_from_saved_state():
workflow = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_standard_manager(manager)
.with_manager(manager=manager)
.with_checkpointing(storage)
.build()
)
@@ -678,7 +678,7 @@ async def test_magentic_checkpoint_resume_from_saved_state():
resumed_workflow = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_standard_manager(InvokeOnceManager())
.with_manager(manager=InvokeOnceManager())
.with_checkpointing(storage)
.build()
)
@@ -699,7 +699,7 @@ async def test_magentic_checkpoint_resume_rejects_participant_renames():
workflow = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_standard_manager(manager)
.with_manager(manager=manager)
.with_plan_review()
.with_checkpointing(storage)
.build()
@@ -719,7 +719,7 @@ async def test_magentic_checkpoint_resume_rejects_participant_renames():
renamed_workflow = (
MagenticBuilder()
.participants([StubThreadAgent(name="renamedAgent")])
.with_standard_manager(InvokeOnceManager())
.with_manager(manager=InvokeOnceManager())
.with_plan_review()
.with_checkpointing(storage)
.build()
@@ -759,7 +759,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_standard_manager(manager).build()
wf = MagenticBuilder().participants([DummyExec("agentA")]).with_manager(manager=manager).build()
events: list[WorkflowEvent] = []
async for ev in wf.run_stream("test limits"):
@@ -784,7 +784,7 @@ async def test_magentic_checkpoint_runtime_only() -> None:
storage = InMemoryCheckpointStorage()
manager = FakeManager(max_round_count=10)
wf = MagenticBuilder().participants([DummyExec("agentA")]).with_standard_manager(manager).build()
wf = MagenticBuilder().participants([DummyExec("agentA")]).with_manager(manager=manager).build()
baseline_output: ChatMessage | None = None
async for ev in wf.run_stream("runtime checkpoint test", checkpoint_storage=storage):
@@ -819,7 +819,7 @@ async def test_magentic_checkpoint_runtime_overrides_buildtime() -> None:
wf = (
MagenticBuilder()
.participants([DummyExec("agentA")])
.with_standard_manager(manager)
.with_manager(manager=manager)
.with_checkpointing(buildtime_storage)
.build()
)
@@ -874,7 +874,7 @@ async def test_magentic_checkpoint_restore_no_duplicate_history():
wf = (
MagenticBuilder()
.participants([DummyExec("agentA")])
.with_standard_manager(manager)
.with_manager(manager=manager)
.with_checkpointing(storage)
.build()
)
@@ -927,3 +927,374 @@ async def test_magentic_checkpoint_restore_no_duplicate_history():
# endregion
# region Participant Factory Tests
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([])
with pytest.raises(
ValueError,
match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first\.",
):
MagenticBuilder().with_manager(manager=FakeManager()).build()
def test_magentic_builder_rejects_mixing_participants_and_factories():
"""Test that mixing .participants() and .register_participants() 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")])
)
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")])
)
async def test_magentic_with_participant_factories():
"""Test workflow creation using participant_factories."""
call_count = 0
def create_agent() -> StubAgent:
nonlocal call_count
call_count += 1
return StubAgent("agentA", "reply from agentA")
manager = FakeManager()
workflow = MagenticBuilder().register_participants([create_agent]).with_manager(manager=manager).build()
# Factory should be called during build
assert call_count == 1
outputs: list[WorkflowOutputEvent] = []
async for event in workflow.run_stream("test task"):
if isinstance(event, WorkflowOutputEvent):
outputs.append(event)
assert len(outputs) == 1
async def test_magentic_participant_factories_reusable_builder():
"""Test that the builder can be reused to build multiple workflows with factories."""
call_count = 0
def create_agent() -> StubAgent:
nonlocal call_count
call_count += 1
return StubAgent("agentA", "reply from agentA")
builder = MagenticBuilder().register_participants([create_agent]).with_manager(manager=FakeManager())
# Build first workflow
wf1 = builder.build()
assert call_count == 1
# Build second workflow
wf2 = builder.build()
assert call_count == 2
# Verify that the two workflows have different agent instances
assert wf1.executors["agentA"] is not wf2.executors["agentA"]
async def test_magentic_participant_factories_with_checkpointing():
"""Test checkpointing with participant_factories."""
storage = InMemoryCheckpointStorage()
def create_agent() -> StubAgent:
return StubAgent("agentA", "reply from agentA")
manager = FakeManager()
workflow = (
MagenticBuilder()
.register_participants([create_agent])
.with_manager(manager=manager)
.with_checkpointing(storage)
.build()
)
outputs: list[WorkflowOutputEvent] = []
async for event in workflow.run_stream("checkpoint test"):
if isinstance(event, WorkflowOutputEvent):
outputs.append(event)
assert outputs, "Should have workflow output"
checkpoints = await storage.list_checkpoints()
assert checkpoints, "Checkpoints should be created during workflow execution"
# endregion
# region Manager Factory Tests
def test_magentic_builder_rejects_multiple_manager_configurations():
"""Test that configuring multiple managers raises ValueError."""
manager = FakeManager()
builder = MagenticBuilder().with_manager(manager=manager)
with pytest.raises(ValueError, match=r"with_manager\(\) has already been called"):
builder.with_manager(manager=manager)
def test_magentic_builder_requires_exactly_one_manager_option():
"""Test that exactly one manager option must be provided."""
manager = FakeManager()
def manager_factory() -> MagenticManagerBase:
return FakeManager()
# No options provided
with pytest.raises(ValueError, match="Exactly one of"):
MagenticBuilder().with_manager() # type: ignore
# Multiple options provided
with pytest.raises(ValueError, match="Exactly one of"):
MagenticBuilder().with_manager(manager=manager, manager_factory=manager_factory) # type: ignore
async def test_magentic_with_manager_factory():
"""Test workflow creation using manager_factory."""
factory_call_count = 0
def manager_factory() -> MagenticManagerBase:
nonlocal factory_call_count
factory_call_count += 1
return FakeManager()
agent = StubAgent("agentA", "reply from agentA")
workflow = MagenticBuilder().participants([agent]).with_manager(manager_factory=manager_factory).build()
# Factory should be called during build
assert factory_call_count == 1
outputs: list[WorkflowOutputEvent] = []
async for event in workflow.run_stream("test task"):
if isinstance(event, WorkflowOutputEvent):
outputs.append(event)
assert len(outputs) == 1
async def test_magentic_with_agent_factory():
"""Test workflow creation using agent_factory for StandardMagenticManager."""
factory_call_count = 0
def agent_factory() -> AgentProtocol:
nonlocal factory_call_count
factory_call_count += 1
return cast(AgentProtocol, StubManagerAgent())
participant = StubAgent("agentA", "reply from agentA")
workflow = (
MagenticBuilder()
.participants([participant])
.with_manager(agent_factory=agent_factory, max_round_count=1)
.build()
)
# Factory should be called during build
assert factory_call_count == 1
# Verify workflow can be started (may not complete successfully due to stub behavior)
event_count = 0
async for _ in workflow.run_stream("test task"):
event_count += 1
if event_count > 10:
break
assert event_count > 0
async def test_magentic_manager_factory_reusable_builder():
"""Test that the builder can be reused to build multiple workflows with manager factory."""
factory_call_count = 0
def manager_factory() -> MagenticManagerBase:
nonlocal factory_call_count
factory_call_count += 1
return FakeManager()
agent = StubAgent("agentA", "reply from agentA")
builder = MagenticBuilder().participants([agent]).with_manager(manager_factory=manager_factory)
# Build first workflow
wf1 = builder.build()
assert factory_call_count == 1
# Build second workflow
wf2 = builder.build()
assert factory_call_count == 2
# Verify that the two workflows have different orchestrator instances
orchestrator1 = next(e for e in wf1.executors.values() if isinstance(e, MagenticOrchestrator))
orchestrator2 = next(e for e in wf2.executors.values() if isinstance(e, MagenticOrchestrator))
assert orchestrator1 is not orchestrator2
def test_magentic_with_both_participant_and_manager_factories():
"""Test workflow creation using both participant_factories and manager_factory."""
participant_factory_call_count = 0
manager_factory_call_count = 0
def create_agent() -> StubAgent:
nonlocal participant_factory_call_count
participant_factory_call_count += 1
return StubAgent("agentA", "reply from agentA")
def manager_factory() -> MagenticManagerBase:
nonlocal manager_factory_call_count
manager_factory_call_count += 1
return FakeManager()
workflow = (
MagenticBuilder().register_participants([create_agent]).with_manager(manager_factory=manager_factory).build()
)
# All factories should be called during build
assert participant_factory_call_count == 1
assert manager_factory_call_count == 1
# Verify executor is present in the workflow
assert "agentA" in workflow.executors
async def test_magentic_factories_reusable_for_multiple_workflows():
"""Test that both factories are reused correctly for multiple workflow builds."""
participant_factory_call_count = 0
manager_factory_call_count = 0
def create_agent() -> StubAgent:
nonlocal participant_factory_call_count
participant_factory_call_count += 1
return StubAgent("agentA", "reply from agentA")
def manager_factory() -> MagenticManagerBase:
nonlocal manager_factory_call_count
manager_factory_call_count += 1
return FakeManager()
builder = MagenticBuilder().register_participants([create_agent]).with_manager(manager_factory=manager_factory)
# Build first workflow
wf1 = builder.build()
assert participant_factory_call_count == 1
assert manager_factory_call_count == 1
# Build second workflow
wf2 = builder.build()
assert participant_factory_call_count == 2
assert manager_factory_call_count == 2
# Verify that the workflows have different agent and orchestrator instances
assert wf1.executors["agentA"] is not wf2.executors["agentA"]
orchestrator1 = next(e for e in wf1.executors.values() if isinstance(e, MagenticOrchestrator))
orchestrator2 = next(e for e in wf2.executors.values() if isinstance(e, MagenticOrchestrator))
assert orchestrator1 is not orchestrator2
def test_magentic_agent_factory_with_standard_manager_options():
"""Test that agent_factory properly passes through standard manager options."""
factory_call_count = 0
def agent_factory() -> AgentProtocol:
nonlocal factory_call_count
factory_call_count += 1
return cast(AgentProtocol, StubManagerAgent())
# Custom options to verify they are passed through
custom_max_stall_count = 5
custom_max_reset_count = 2
custom_max_round_count = 10
custom_facts_prompt = "Custom facts prompt: {task}"
custom_plan_prompt = "Custom plan prompt: {team}"
custom_full_prompt = "Custom full prompt: {task} {team} {facts} {plan}"
custom_facts_update_prompt = "Custom facts update: {task} {old_facts}"
custom_plan_update_prompt = "Custom plan update: {team}"
custom_progress_prompt = "Custom progress: {task} {team} {names}"
custom_final_prompt = "Custom final: {task}"
# Create a custom task ledger
from agent_framework._workflows._magentic import _MagenticTaskLedger
custom_task_ledger = _MagenticTaskLedger(
facts=ChatMessage(role=Role.ASSISTANT, text="Custom facts"),
plan=ChatMessage(role=Role.ASSISTANT, text="Custom plan"),
)
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()
)
# Factory should be called during build
assert factory_call_count == 1
# Get the orchestrator and verify the manager has the custom options
orchestrator = next(e for e in workflow.executors.values() if isinstance(e, MagenticOrchestrator))
manager = orchestrator._manager # type: ignore[reportPrivateUsage]
# Verify the manager is a StandardMagenticManager with the expected options
from agent_framework import StandardMagenticManager
assert isinstance(manager, StandardMagenticManager)
assert manager.task_ledger is custom_task_ledger
assert manager.max_stall_count == custom_max_stall_count
assert manager.max_reset_count == custom_max_reset_count
assert manager.max_round_count == custom_max_round_count
assert manager.task_ledger_facts_prompt == custom_facts_prompt
assert manager.task_ledger_plan_prompt == custom_plan_prompt
assert manager.task_ledger_full_prompt == custom_full_prompt
assert manager.task_ledger_facts_update_prompt == custom_facts_update_prompt
assert manager.task_ledger_plan_update_prompt == custom_plan_update_prompt
assert manager.progress_ledger_prompt == custom_progress_prompt
assert manager.final_answer_prompt == custom_final_prompt
# endregion
@@ -1,6 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncIterable
from collections.abc import AsyncIterable, Sequence
from typing import Annotated, Any
import pytest
@@ -51,7 +51,7 @@ class _KwargsCapturingAgent(BaseAgent):
async def run(
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
@@ -61,7 +61,7 @@ class _KwargsCapturingAgent(BaseAgent):
async def run_stream(
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
@@ -187,7 +187,7 @@ async def test_groupchat_kwargs_flow_to_agents() -> None:
workflow = (
GroupChatBuilder()
.participants([agent1, agent2])
.with_select_speaker_func(simple_selector)
.with_orchestrator(selection_func=simple_selector)
.with_max_rounds(2) # Limit rounds to prevent infinite loop
.build()
)
@@ -408,7 +408,7 @@ async def test_magentic_kwargs_flow_to_agents() -> None:
agent = _KwargsCapturingAgent(name="agent1")
manager = _MockManager()
workflow = MagenticBuilder().participants([agent]).with_standard_manager(manager=manager).build()
workflow = MagenticBuilder().participants([agent]).with_manager(manager=manager).build()
custom_data = {"session_id": "magentic123"}
@@ -457,7 +457,7 @@ async def test_magentic_kwargs_stored_in_shared_state() -> None:
agent = _KwargsCapturingAgent(name="agent1")
manager = _MockManager()
magentic_workflow = MagenticBuilder().participants([agent]).with_standard_manager(manager=manager).build()
magentic_workflow = MagenticBuilder().participants([agent]).with_manager(manager=manager).build()
# Use MagenticWorkflow.run_stream() which goes through the kwargs attachment path
custom_data = {"magentic_key": "magentic_value"}