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
@@ -52,12 +52,10 @@ Orchestrator-directed multi-agent conversations:
|
||||
```python
|
||||
from agent_framework_orchestrations import GroupChatBuilder
|
||||
|
||||
workflow = (
|
||||
GroupChatBuilder()
|
||||
.with_orchestrator(selection_func=my_selector)
|
||||
.participants([agent1, agent2])
|
||||
.build()
|
||||
)
|
||||
workflow = GroupChatBuilder(
|
||||
participants=[agent1, agent2],
|
||||
selection_func=my_selector,
|
||||
).build()
|
||||
```
|
||||
|
||||
### MagenticBuilder
|
||||
@@ -67,12 +65,10 @@ Sophisticated multi-agent orchestration using the Magentic One pattern:
|
||||
```python
|
||||
from agent_framework_orchestrations import MagenticBuilder
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants([researcher, writer, reviewer])
|
||||
.with_manager(agent=manager_agent)
|
||||
.build()
|
||||
)
|
||||
workflow = MagenticBuilder(
|
||||
participants=[researcher, writer, reviewer],
|
||||
manager_agent=manager_agent,
|
||||
).build()
|
||||
```
|
||||
|
||||
## Usage with agent_framework
|
||||
|
||||
@@ -29,8 +29,8 @@ parallel workflow with:
|
||||
- a default aggregator that combines all agent conversations and completes the workflow
|
||||
|
||||
Notes:
|
||||
- Participants can be provided as SupportsAgentRun or Executor instances via `.participants()`,
|
||||
or as factories returning SupportsAgentRun or Executor via `.register_participants()`.
|
||||
- Participants can be provided as SupportsAgentRun or Executor instances via `participants=[...]`,
|
||||
or as factories returning SupportsAgentRun or Executor via `participant_factories=[...]`.
|
||||
- A custom aggregator can be provided as:
|
||||
- an Executor instance (it should handle list[AgentExecutorResponse],
|
||||
yield output), or
|
||||
@@ -186,8 +186,8 @@ class _CallbackAggregator(Executor):
|
||||
class ConcurrentBuilder:
|
||||
r"""High-level builder for concurrent agent workflows.
|
||||
|
||||
- `participants([...])` accepts a list of SupportsAgentRun (recommended) or Executor.
|
||||
- `register_participants([...])` accepts a list of factories for SupportsAgentRun (recommended)
|
||||
- `participants=[...]` accepts a list of SupportsAgentRun (recommended) or Executor.
|
||||
- `participant_factories=[...]` accepts a list of factories for SupportsAgentRun (recommended)
|
||||
or Executor factories
|
||||
- `build()` wires: dispatcher -> fan-out -> participants -> fan-in -> aggregator.
|
||||
- `with_aggregator(...)` overrides the default aggregator with an Executor or callback.
|
||||
@@ -200,10 +200,10 @@ class ConcurrentBuilder:
|
||||
from agent_framework_orchestrations import ConcurrentBuilder
|
||||
|
||||
# Minimal: use default aggregator (returns list[ChatMessage])
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2, agent3]).build()
|
||||
workflow = ConcurrentBuilder(participants=[agent1, agent2, agent3]).build()
|
||||
|
||||
# With agent factories
|
||||
workflow = ConcurrentBuilder().register_participants([create_agent1, create_agent2, create_agent3]).build()
|
||||
workflow = ConcurrentBuilder(participant_factories=[create_agent1, create_agent2, create_agent3]).build()
|
||||
|
||||
|
||||
# Custom aggregator via callback (sync or async). The callback receives
|
||||
@@ -212,7 +212,7 @@ class ConcurrentBuilder:
|
||||
return " | ".join(r.agent_response.messages[-1].text for r in results)
|
||||
|
||||
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2, agent3]).with_aggregator(summarize).build()
|
||||
workflow = ConcurrentBuilder(participants=[agent1, agent2, agent3]).with_aggregator(summarize).build()
|
||||
|
||||
|
||||
# Custom aggregator via a factory
|
||||
@@ -223,112 +223,76 @@ class ConcurrentBuilder:
|
||||
|
||||
|
||||
workflow = (
|
||||
ConcurrentBuilder()
|
||||
.register_participants([create_agent1, create_agent2, create_agent3])
|
||||
ConcurrentBuilder(participant_factories=[create_agent1, create_agent2, create_agent3])
|
||||
.register_aggregator(lambda: MyAggregator(id="my_aggregator"))
|
||||
.build()
|
||||
)
|
||||
|
||||
|
||||
# Enable checkpoint persistence so runs can resume
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2, agent3]).with_checkpointing(storage).build()
|
||||
workflow = ConcurrentBuilder(participants=[agent1, agent2, agent3], checkpoint_storage=storage).build()
|
||||
|
||||
# Enable request info before aggregation
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2]).with_request_info().build()
|
||||
workflow = ConcurrentBuilder(participants=[agent1, agent2]).with_request_info().build()
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
participants: Sequence[SupportsAgentRun | Executor] | None = None,
|
||||
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]] | None = None,
|
||||
checkpoint_storage: CheckpointStorage | None = None,
|
||||
intermediate_outputs: bool = False,
|
||||
) -> None:
|
||||
"""Initialize the ConcurrentBuilder.
|
||||
|
||||
Args:
|
||||
participants: Optional sequence of agent or executor instances to run in parallel.
|
||||
participant_factories: Optional sequence of callables returning agent or executor instances.
|
||||
checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence.
|
||||
intermediate_outputs: If True, enables intermediate outputs from agent participants
|
||||
before aggregation.
|
||||
"""
|
||||
self._participants: list[SupportsAgentRun | Executor] = []
|
||||
self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = []
|
||||
self._aggregator: Executor | None = None
|
||||
self._aggregator_factory: Callable[[], Executor] | None = None
|
||||
self._checkpoint_storage: CheckpointStorage | None = None
|
||||
self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage
|
||||
self._request_info_enabled: bool = False
|
||||
self._request_info_filter: set[str] | None = None
|
||||
self._intermediate_outputs: bool = False
|
||||
self._intermediate_outputs: bool = intermediate_outputs
|
||||
|
||||
def register_participants(
|
||||
if participants is None and participant_factories is None:
|
||||
raise ValueError("Either participants or participant_factories must be provided.")
|
||||
|
||||
if participant_factories is not None:
|
||||
self._set_participant_factories(participant_factories)
|
||||
if participants is not None:
|
||||
self._set_participants(participants)
|
||||
|
||||
def _set_participant_factories(
|
||||
self,
|
||||
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]],
|
||||
) -> "ConcurrentBuilder":
|
||||
r"""Define the parallel participants for this concurrent workflow.
|
||||
|
||||
Accepts factories (callables) that return SupportsAgentRun instances (e.g., created
|
||||
by a chat client) or Executor instances. Each participant created by a factory
|
||||
is wired as a parallel branch using fan-out edges from an internal dispatcher.
|
||||
|
||||
Args:
|
||||
participant_factories: Sequence of callables returning SupportsAgentRun or Executor instances
|
||||
|
||||
Raises:
|
||||
ValueError: if `participant_factories` is empty or `.participants()`
|
||||
or `.register_participants()` were already called
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def create_researcher() -> ChatAgent:
|
||||
return ...
|
||||
|
||||
|
||||
def create_marketer() -> ChatAgent:
|
||||
return ...
|
||||
|
||||
|
||||
def create_legal() -> ChatAgent:
|
||||
return ...
|
||||
|
||||
|
||||
class MyCustomExecutor(Executor): ...
|
||||
|
||||
|
||||
wf = ConcurrentBuilder().register_participants([create_researcher, create_marketer, create_legal]).build()
|
||||
|
||||
# Mixing agent(s) and executor(s) is supported
|
||||
wf2 = ConcurrentBuilder().register_participants([create_researcher, MyCustomExecutor]).build()
|
||||
"""
|
||||
) -> None:
|
||||
"""Set participant factories (internal)."""
|
||||
if self._participants:
|
||||
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
|
||||
raise ValueError("Cannot provide both participants and participant_factories.")
|
||||
|
||||
if self._participant_factories:
|
||||
raise ValueError("register_participants() has already been called on this builder instance.")
|
||||
raise ValueError("participant_factories already set.")
|
||||
|
||||
if not participant_factories:
|
||||
raise ValueError("participant_factories cannot be empty")
|
||||
|
||||
self._participant_factories = list(participant_factories)
|
||||
return self
|
||||
|
||||
def participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> "ConcurrentBuilder":
|
||||
r"""Define the parallel participants for this concurrent workflow.
|
||||
|
||||
Accepts SupportsAgentRun instances (e.g., created by a chat client) or Executor
|
||||
instances. Each participant is wired as a parallel branch using fan-out edges
|
||||
from an internal dispatcher.
|
||||
|
||||
Args:
|
||||
participants: Sequence of SupportsAgentRun or Executor instances
|
||||
|
||||
Raises:
|
||||
ValueError: if `participants` is empty, contains duplicates, or `.register_participants()`
|
||||
or `.participants()` were already called
|
||||
TypeError: if any entry is not SupportsAgentRun or Executor
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
wf = ConcurrentBuilder().participants([researcher_agent, marketer_agent, legal_agent]).build()
|
||||
|
||||
# Mixing agent(s) and executor(s) is supported
|
||||
wf2 = ConcurrentBuilder().participants([researcher_agent, my_custom_executor]).build()
|
||||
"""
|
||||
def _set_participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> None:
|
||||
"""Set participants (internal)."""
|
||||
if self._participant_factories:
|
||||
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
|
||||
raise ValueError("Cannot provide both participants and participant_factories.")
|
||||
|
||||
if self._participants:
|
||||
raise ValueError("participants() has already been called on this builder instance.")
|
||||
raise ValueError("participants already set.")
|
||||
|
||||
if not participants:
|
||||
raise ValueError("participants cannot be empty")
|
||||
@@ -350,7 +314,6 @@ class ConcurrentBuilder:
|
||||
raise TypeError(f"participants must be SupportsAgentRun or Executor instances; got {type(p).__name__}")
|
||||
|
||||
self._participants = list(participants)
|
||||
return self
|
||||
|
||||
def register_aggregator(self, aggregator_factory: Callable[[], Executor]) -> "ConcurrentBuilder":
|
||||
r"""Define a custom aggregator for this concurrent workflow.
|
||||
@@ -412,7 +375,7 @@ class ConcurrentBuilder:
|
||||
await ctx.yield_output(" | ".join(r.agent_response.messages[-1].text for r in results))
|
||||
|
||||
|
||||
wf = ConcurrentBuilder().participants([a1, a2, a3]).with_aggregator(CustomAggregator()).build()
|
||||
wf = ConcurrentBuilder(participants=[a1, a2, a3]).with_aggregator(CustomAggregator()).build()
|
||||
|
||||
|
||||
# Callback-based aggregator (string result)
|
||||
@@ -420,7 +383,7 @@ class ConcurrentBuilder:
|
||||
return " | ".join(r.agent_response.messages[-1].text for r in results)
|
||||
|
||||
|
||||
wf = ConcurrentBuilder().participants([a1, a2, a3]).with_aggregator(summarize).build()
|
||||
wf = ConcurrentBuilder(participants=[a1, a2, a3]).with_aggregator(summarize).build()
|
||||
|
||||
|
||||
# Callback-based aggregator (yield result)
|
||||
@@ -428,7 +391,7 @@ class ConcurrentBuilder:
|
||||
await ctx.yield_output(" | ".join(r.agent_response.messages[-1].text for r in results))
|
||||
|
||||
|
||||
wf = ConcurrentBuilder().participants([a1, a2, a3]).with_aggregator(summarize).build()
|
||||
wf = ConcurrentBuilder(participants=[a1, a2, a3]).with_aggregator(summarize).build()
|
||||
"""
|
||||
if self._aggregator_factory is not None:
|
||||
raise ValueError(
|
||||
@@ -447,15 +410,6 @@ class ConcurrentBuilder:
|
||||
|
||||
return self
|
||||
|
||||
def with_checkpointing(self, checkpoint_storage: CheckpointStorage) -> "ConcurrentBuilder":
|
||||
"""Enable checkpoint persistence using the provided storage backend.
|
||||
|
||||
Args:
|
||||
checkpoint_storage: CheckpointStorage instance for persisting workflow state
|
||||
"""
|
||||
self._checkpoint_storage = checkpoint_storage
|
||||
return self
|
||||
|
||||
def with_request_info(
|
||||
self,
|
||||
*,
|
||||
@@ -489,23 +443,10 @@ class ConcurrentBuilder:
|
||||
|
||||
return self
|
||||
|
||||
def with_intermediate_outputs(self) -> "ConcurrentBuilder":
|
||||
"""Enable intermediate outputs from agent participants before aggregation.
|
||||
|
||||
When enabled, the workflow returns each agent participant's response or yields
|
||||
streaming updates as they become available. The output of the aggregator will
|
||||
always be available as the final output of the workflow.
|
||||
|
||||
Returns:
|
||||
Self for fluent chaining
|
||||
"""
|
||||
self._intermediate_outputs = True
|
||||
return self
|
||||
|
||||
def _resolve_participants(self) -> list[Executor]:
|
||||
"""Resolve participant instances into Executor objects."""
|
||||
if not self._participants and not self._participant_factories:
|
||||
raise ValueError("No participants provided. Call .participants() or .register_participants() first.")
|
||||
raise ValueError("No participants provided. Pass participants or participant_factories to the constructor.")
|
||||
# We don't need to check if both are set since that is handled in the respective methods
|
||||
|
||||
participants: list[Executor | SupportsAgentRun] = []
|
||||
@@ -557,7 +498,7 @@ class ConcurrentBuilder:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2]).build()
|
||||
workflow = ConcurrentBuilder(participants=[agent1, agent2]).build()
|
||||
"""
|
||||
# Internal nodes
|
||||
dispatcher = _DispatchToAllParticipants(id="dispatcher")
|
||||
@@ -574,18 +515,14 @@ class ConcurrentBuilder:
|
||||
# Resolve participants and participant factories to executors
|
||||
participants: list[Executor] = self._resolve_participants()
|
||||
|
||||
builder = WorkflowBuilder()
|
||||
builder.set_start_executor(dispatcher)
|
||||
builder = WorkflowBuilder(
|
||||
start_executor=dispatcher,
|
||||
checkpoint_storage=self._checkpoint_storage,
|
||||
output_executors=[aggregator] if not self._intermediate_outputs else None,
|
||||
)
|
||||
# Fan-out for parallel execution
|
||||
builder.add_fan_out_edges(dispatcher, participants)
|
||||
# Direct fan-in to aggregator
|
||||
builder.add_fan_in_edges(participants, aggregator)
|
||||
|
||||
if not self._intermediate_outputs:
|
||||
# Constrain output to aggregator only
|
||||
builder = builder.with_output_from([aggregator])
|
||||
|
||||
if self._checkpoint_storage is not None:
|
||||
builder = builder.with_checkpointing(self._checkpoint_storage)
|
||||
|
||||
return builder.build()
|
||||
|
||||
@@ -24,7 +24,7 @@ import sys
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, ClassVar, cast, overload
|
||||
from typing import Any, ClassVar, cast
|
||||
|
||||
from agent_framework import ChatAgent, SupportsAgentRun
|
||||
from agent_framework._threads import AgentThread
|
||||
@@ -521,8 +521,39 @@ class GroupChatBuilder:
|
||||
|
||||
DEFAULT_ORCHESTRATOR_ID: ClassVar[str] = "group_chat_orchestrator"
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the GroupChatBuilder."""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
participants: Sequence[SupportsAgentRun | Executor] | None = None,
|
||||
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]] | None = None,
|
||||
# Orchestrator config (exactly one required)
|
||||
orchestrator_agent: ChatAgent | Callable[[], ChatAgent] | None = None,
|
||||
orchestrator: BaseGroupChatOrchestrator | Callable[[], BaseGroupChatOrchestrator] | None = None,
|
||||
selection_func: GroupChatSelectionFunction | None = None,
|
||||
orchestrator_name: str | None = None,
|
||||
# Existing params
|
||||
termination_condition: TerminationCondition | None = None,
|
||||
max_rounds: int | None = None,
|
||||
checkpoint_storage: CheckpointStorage | None = None,
|
||||
intermediate_outputs: bool = False,
|
||||
) -> None:
|
||||
"""Initialize the GroupChatBuilder.
|
||||
|
||||
Args:
|
||||
participants: Optional sequence of agent or executor instances for the group chat.
|
||||
participant_factories: Optional sequence of callables returning agent or executor instances.
|
||||
orchestrator_agent: An instance of ChatAgent or a callable that produces one to manage the group chat.
|
||||
orchestrator: An instance of BaseGroupChatOrchestrator or a callable that produces one to manage the
|
||||
group chat.
|
||||
selection_func: Callable that receives the current GroupChatState and returns the name of the next
|
||||
participant to speak.
|
||||
orchestrator_name: Optional display name for the orchestrator when using a selection function.
|
||||
termination_condition: Optional callable that receives the conversation history and returns
|
||||
True to terminate the conversation, False to continue.
|
||||
max_rounds: Optional maximum number of orchestrator rounds to prevent infinite conversations.
|
||||
checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence.
|
||||
intermediate_outputs: If True, enables intermediate outputs from agent participants.
|
||||
"""
|
||||
self._participants: dict[str, SupportsAgentRun | Executor] = {}
|
||||
self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = []
|
||||
|
||||
@@ -531,96 +562,49 @@ class GroupChatBuilder:
|
||||
self._orchestrator_factory: Callable[[], ChatAgent | BaseGroupChatOrchestrator] | None = None
|
||||
self._selection_func: GroupChatSelectionFunction | None = None
|
||||
self._agent_orchestrator: ChatAgent | None = None
|
||||
self._termination_condition: TerminationCondition | None = None
|
||||
self._max_rounds: int | None = None
|
||||
self._termination_condition: TerminationCondition | None = termination_condition
|
||||
self._max_rounds: int | None = max_rounds
|
||||
self._orchestrator_name: str | None = None
|
||||
|
||||
# Checkpoint related members
|
||||
self._checkpoint_storage: CheckpointStorage | None = None
|
||||
self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage
|
||||
|
||||
# Request info related members
|
||||
self._request_info_enabled: bool = False
|
||||
self._request_info_filter: set[str] = set()
|
||||
|
||||
# Intermediate outputs
|
||||
self._intermediate_outputs = False
|
||||
self._intermediate_outputs = intermediate_outputs
|
||||
|
||||
@overload
|
||||
def with_orchestrator(self, *, agent: ChatAgent | Callable[[], ChatAgent]) -> "GroupChatBuilder":
|
||||
"""Set the orchestrator for this group chat workflow using a ChatAgent.
|
||||
if participants is None and participant_factories is None:
|
||||
raise ValueError("Either participants or participant_factories must be provided.")
|
||||
|
||||
Args:
|
||||
agent: An instance of ChatAgent or a callable that produces one to manage the group chat.
|
||||
if participant_factories is not None:
|
||||
self._set_participant_factories(participant_factories)
|
||||
if participants is not None:
|
||||
self._set_participants(participants)
|
||||
|
||||
Returns:
|
||||
Self for fluent chaining.
|
||||
"""
|
||||
...
|
||||
# Set orchestrator if provided
|
||||
if any(x is not None for x in [orchestrator_agent, orchestrator, selection_func]):
|
||||
self._set_orchestrator(
|
||||
orchestrator_agent=orchestrator_agent,
|
||||
orchestrator=orchestrator,
|
||||
selection_func=selection_func,
|
||||
orchestrator_name=orchestrator_name,
|
||||
)
|
||||
|
||||
@overload
|
||||
def with_orchestrator(
|
||||
self, *, orchestrator: BaseGroupChatOrchestrator | Callable[[], BaseGroupChatOrchestrator]
|
||||
) -> "GroupChatBuilder":
|
||||
"""Set the orchestrator for this group chat workflow using a custom orchestrator.
|
||||
|
||||
Args:
|
||||
orchestrator: An instance of BaseGroupChatOrchestrator or a callable that produces one to
|
||||
manage the group chat.
|
||||
|
||||
Returns:
|
||||
Self for fluent chaining.
|
||||
|
||||
Note:
|
||||
When using a custom orchestrator that implements `BaseGroupChatOrchestrator`, setting
|
||||
`termination_condition` and `max_rounds` on the builder will have no effect since the
|
||||
orchestrator is already fully defined.
|
||||
"""
|
||||
...
|
||||
|
||||
@overload
|
||||
def with_orchestrator(
|
||||
def _set_orchestrator(
|
||||
self,
|
||||
*,
|
||||
selection_func: GroupChatSelectionFunction,
|
||||
orchestrator_name: str | None = None,
|
||||
) -> "GroupChatBuilder":
|
||||
"""Set the orchestrator for this group chat workflow using a selection function.
|
||||
|
||||
Args:
|
||||
selection_func: Callable that receives the current GroupChatState and returns
|
||||
the name of the next participant to speak, or None to finish.
|
||||
orchestrator_name: Optional display name for the orchestrator in the workflow.
|
||||
If not provided, defaults to `GroupChatBuilder.DEFAULT_ORCHESTRATOR_ID`.
|
||||
|
||||
Returns:
|
||||
Self for fluent chaining.
|
||||
"""
|
||||
...
|
||||
|
||||
def with_orchestrator(
|
||||
self,
|
||||
*,
|
||||
agent: ChatAgent | Callable[[], ChatAgent] | None = None,
|
||||
orchestrator_agent: ChatAgent | Callable[[], ChatAgent] | None = None,
|
||||
orchestrator: BaseGroupChatOrchestrator | Callable[[], BaseGroupChatOrchestrator] | None = None,
|
||||
selection_func: GroupChatSelectionFunction | None = None,
|
||||
orchestrator_name: str | None = None,
|
||||
) -> "GroupChatBuilder":
|
||||
"""Set the orchestrator for this group chat workflow.
|
||||
|
||||
An group chat orchestrator is responsible for managing the flow of conversation, making
|
||||
sure all participants are synced and picking the next speaker according to the defined logic
|
||||
until the termination conditions are met.
|
||||
|
||||
There are a few ways to configure the orchestrator:
|
||||
1. Provide a ChatAgent instance or a factory function that produces one to use an agent-based orchestrator
|
||||
2. Provide a BaseGroupChatOrchestrator instance or a factory function that produces one to use a custom
|
||||
orchestrator
|
||||
3. Provide a selection function to use that picks the next speaker based on the function logic
|
||||
|
||||
You can only use one of the above methods to configure the orchestrator.
|
||||
) -> None:
|
||||
"""Set the orchestrator for this group chat workflow (internal).
|
||||
|
||||
Args:
|
||||
agent: An instance of ChatAgent or a callable that produces one to manage the group chat.
|
||||
orchestrator_agent: An instance of ChatAgent or a callable that produces one to manage the group chat.
|
||||
orchestrator: An instance of BaseGroupChatOrchestrator or a callable that produces one to manage the group
|
||||
chat.
|
||||
selection_func: Callable that receives the current GroupChatState and returns
|
||||
@@ -630,121 +614,58 @@ class GroupChatBuilder:
|
||||
`GroupChatBuilder.DEFAULT_ORCHESTRATOR_ID`. This parameter is
|
||||
ignored if using an agent or custom orchestrator.
|
||||
|
||||
Returns:
|
||||
Self for fluent chaining.
|
||||
|
||||
Raises:
|
||||
ValueError: If an orchestrator has already been set or if none or multiple
|
||||
of the parameters are provided.
|
||||
|
||||
Note:
|
||||
When using a custom orchestrator that implements `BaseGroupChatOrchestrator`, either
|
||||
via the `orchestrator` or `orchestrator_factory` parameters, setting `termination_condition`
|
||||
and `max_rounds` on the builder will have no effect since the orchestrator is already
|
||||
fully defined.
|
||||
|
||||
Example:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework_orchestrations import GroupChatBuilder
|
||||
|
||||
|
||||
orchestrator = CustomGroupChatOrchestrator(...)
|
||||
workflow = GroupChatBuilder().with_orchestrator(orchestrator).participants([agent1, agent2]).build()
|
||||
"""
|
||||
if self._agent_orchestrator is not None:
|
||||
raise ValueError(
|
||||
"An agent orchestrator has already been configured. Call with_orchestrator(...) once only."
|
||||
)
|
||||
raise ValueError("An agent orchestrator has already been configured. Set orchestrator config once only.")
|
||||
|
||||
if self._orchestrator is not None:
|
||||
raise ValueError("An orchestrator has already been configured. Call with_orchestrator(...) once only.")
|
||||
raise ValueError("An orchestrator has already been configured. Set orchestrator config once only.")
|
||||
|
||||
if self._orchestrator_factory is not None:
|
||||
raise ValueError("A factory has already been configured. Call with_orchestrator(...) once only.")
|
||||
raise ValueError("A factory has already been configured. Set orchestrator config once only.")
|
||||
|
||||
if self._selection_func is not None:
|
||||
raise ValueError("A selection function has already been configured. Call with_orchestrator(...) once only.")
|
||||
raise ValueError("A selection function has already been configured. Set orchestrator config once only.")
|
||||
|
||||
if sum(x is not None for x in [agent, orchestrator, selection_func]) != 1:
|
||||
raise ValueError("Exactly one of agent, orchestrator, or selection_func must be provided.")
|
||||
if sum(x is not None for x in [orchestrator_agent, orchestrator, selection_func]) != 1:
|
||||
raise ValueError("Exactly one of orchestrator_agent, orchestrator, or selection_func must be provided.")
|
||||
|
||||
if agent is not None and isinstance(agent, ChatAgent):
|
||||
self._agent_orchestrator = agent
|
||||
if orchestrator_agent is not None and isinstance(orchestrator_agent, ChatAgent):
|
||||
self._agent_orchestrator = orchestrator_agent
|
||||
elif orchestrator is not None and isinstance(orchestrator, BaseGroupChatOrchestrator):
|
||||
self._orchestrator = orchestrator
|
||||
elif selection_func is not None:
|
||||
self._selection_func = selection_func
|
||||
self._orchestrator_name = orchestrator_name
|
||||
else:
|
||||
self._orchestrator_factory = agent or orchestrator
|
||||
self._orchestrator_factory = orchestrator_agent or orchestrator
|
||||
|
||||
return self
|
||||
|
||||
def register_participants(
|
||||
def _set_participant_factories(
|
||||
self,
|
||||
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]],
|
||||
) -> "GroupChatBuilder":
|
||||
"""Register participant factories for this group chat workflow.
|
||||
|
||||
Args:
|
||||
participant_factories: Sequence of callables that produce participant definitions
|
||||
when invoked. Each callable should return either an SupportsAgentRun instance
|
||||
(auto-wrapped as AgentExecutor) or an Executor instance.
|
||||
|
||||
Returns:
|
||||
Self for fluent chaining
|
||||
|
||||
Raises:
|
||||
ValueError: If participant_factories is empty, or participants
|
||||
or participant factories are already set
|
||||
"""
|
||||
) -> None:
|
||||
"""Set participant factories (internal)."""
|
||||
if self._participants:
|
||||
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
|
||||
raise ValueError("Cannot provide both participants and participant_factories.")
|
||||
|
||||
if self._participant_factories:
|
||||
raise ValueError("register_participants() has already been called on this builder instance.")
|
||||
raise ValueError("participant_factories already set.")
|
||||
|
||||
if not participant_factories:
|
||||
raise ValueError("participant_factories cannot be empty")
|
||||
|
||||
self._participant_factories = list(participant_factories)
|
||||
return self
|
||||
|
||||
def participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> "GroupChatBuilder":
|
||||
"""Define participants for this group chat workflow.
|
||||
|
||||
Accepts SupportsAgentRun instances (auto-wrapped as AgentExecutor) or Executor instances.
|
||||
|
||||
Args:
|
||||
participants: Sequence of participant definitions
|
||||
|
||||
Returns:
|
||||
Self for fluent chaining
|
||||
|
||||
Raises:
|
||||
ValueError: If participants are empty, names are duplicated, or participants
|
||||
or participant factories are already set
|
||||
TypeError: If any participant is not SupportsAgentRun or Executor instance
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework_orchestrations import GroupChatBuilder
|
||||
|
||||
workflow = (
|
||||
GroupChatBuilder()
|
||||
.with_orchestrator(selection_func=my_selection_function)
|
||||
.participants([agent1, agent2, custom_executor])
|
||||
.build()
|
||||
)
|
||||
"""
|
||||
def _set_participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> None:
|
||||
"""Set participants (internal)."""
|
||||
if self._participant_factories:
|
||||
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
|
||||
raise ValueError("Cannot provide both participants and participant_factories.")
|
||||
|
||||
if self._participants:
|
||||
raise ValueError("participants have already been set. Call participants() at most once.")
|
||||
raise ValueError("participants already set.")
|
||||
|
||||
if not participants:
|
||||
raise ValueError("participants cannot be empty.")
|
||||
@@ -770,8 +691,6 @@ class GroupChatBuilder:
|
||||
|
||||
self._participants = named
|
||||
|
||||
return self
|
||||
|
||||
def with_termination_condition(self, termination_condition: TerminationCondition) -> "GroupChatBuilder":
|
||||
"""Set a custom termination condition for the group chat workflow.
|
||||
|
||||
@@ -797,9 +716,10 @@ class GroupChatBuilder:
|
||||
|
||||
specialist_agent = ...
|
||||
workflow = (
|
||||
GroupChatBuilder()
|
||||
.with_orchestrator(selection_func=my_selection_function)
|
||||
.participants([agent1, specialist_agent])
|
||||
GroupChatBuilder(
|
||||
participants=[agent1, specialist_agent],
|
||||
selection_func=my_selection_function,
|
||||
)
|
||||
.with_termination_condition(stop_after_two_calls)
|
||||
.build()
|
||||
)
|
||||
@@ -851,9 +771,10 @@ class GroupChatBuilder:
|
||||
|
||||
storage = MemoryCheckpointStorage()
|
||||
workflow = (
|
||||
GroupChatBuilder()
|
||||
.with_orchestrator(selection_func=my_selection_function)
|
||||
.participants([agent1, agent2])
|
||||
GroupChatBuilder(
|
||||
participants=[agent1, agent2],
|
||||
selection_func=my_selection_function,
|
||||
)
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
@@ -890,19 +811,6 @@ class GroupChatBuilder:
|
||||
|
||||
return self
|
||||
|
||||
def with_intermediate_outputs(self) -> "GroupChatBuilder":
|
||||
"""Enable intermediate outputs from agent participants.
|
||||
|
||||
When enabled, the workflow returns each agent participant's response or yields
|
||||
streaming updates as they become available. The output of the orchestrator will
|
||||
always be available as the final output of the workflow.
|
||||
|
||||
Returns:
|
||||
Self for fluent chaining
|
||||
"""
|
||||
self._intermediate_outputs = True
|
||||
return self
|
||||
|
||||
def _resolve_orchestrator(self, participants: Sequence[Executor]) -> Executor:
|
||||
"""Determine the orchestrator to use for the workflow.
|
||||
|
||||
@@ -913,8 +821,11 @@ class GroupChatBuilder:
|
||||
x is None
|
||||
for x in [self._agent_orchestrator, self._selection_func, self._orchestrator, self._orchestrator_factory]
|
||||
):
|
||||
raise ValueError("No orchestrator has been configured. Call with_orchestrator() to set one.")
|
||||
# We don't need to check if multiple are set since that is handled in with_orchestrator()
|
||||
raise ValueError(
|
||||
"No orchestrator has been configured. "
|
||||
"Pass orchestrator_agent, orchestrator, or selection_func to the constructor."
|
||||
)
|
||||
# We don't need to check if multiple are set since that is handled in _set_orchestrator()
|
||||
|
||||
if self._agent_orchestrator:
|
||||
return AgentBasedGroupChatOrchestrator(
|
||||
@@ -954,12 +865,15 @@ class GroupChatBuilder:
|
||||
)
|
||||
|
||||
# This should never be reached due to the checks above
|
||||
raise RuntimeError("Orchestrator could not be resolved. Please provide one via with_orchestrator()")
|
||||
raise RuntimeError(
|
||||
"Orchestrator could not be resolved. "
|
||||
"Pass orchestrator_agent, orchestrator, or selection_func to the constructor."
|
||||
)
|
||||
|
||||
def _resolve_participants(self) -> list[Executor]:
|
||||
"""Resolve participant instances into Executor objects."""
|
||||
if not self._participants and not self._participant_factories:
|
||||
raise ValueError("No participants provided. Call .participants() or .register_participants() first.")
|
||||
raise ValueError("No participants provided. Pass participants or participant_factories to the constructor.")
|
||||
# We don't need to check if both are set since that is handled in the respective methods
|
||||
|
||||
participants: list[Executor | SupportsAgentRun] = []
|
||||
@@ -1004,19 +918,16 @@ class GroupChatBuilder:
|
||||
orchestrator: Executor = self._resolve_orchestrator(participants)
|
||||
|
||||
# Build workflow graph
|
||||
workflow_builder = WorkflowBuilder().set_start_executor(orchestrator)
|
||||
workflow_builder = WorkflowBuilder(
|
||||
start_executor=orchestrator,
|
||||
checkpoint_storage=self._checkpoint_storage,
|
||||
output_executors=[orchestrator] if not self._intermediate_outputs else None,
|
||||
)
|
||||
for participant in participants:
|
||||
# Orchestrator and participant bi-directional edges
|
||||
workflow_builder = workflow_builder.add_edge(orchestrator, participant)
|
||||
workflow_builder = workflow_builder.add_edge(participant, orchestrator)
|
||||
|
||||
if not self._intermediate_outputs:
|
||||
# Constrain output to orchestrator only
|
||||
workflow_builder = workflow_builder.with_output_from([orchestrator])
|
||||
|
||||
if self._checkpoint_storage is not None:
|
||||
workflow_builder = workflow_builder.with_checkpointing(self._checkpoint_storage)
|
||||
|
||||
return workflow_builder.build()
|
||||
|
||||
|
||||
|
||||
@@ -577,6 +577,8 @@ class HandoffBuilder:
|
||||
participants: Sequence[SupportsAgentRun] | None = None,
|
||||
participant_factories: Mapping[str, Callable[[], SupportsAgentRun]] | None = None,
|
||||
description: str | None = None,
|
||||
checkpoint_storage: CheckpointStorage | None = None,
|
||||
termination_condition: TerminationCondition | None = None,
|
||||
) -> None:
|
||||
r"""Initialize a HandoffBuilder for creating conversational handoff workflows.
|
||||
|
||||
@@ -599,6 +601,9 @@ class HandoffBuilder:
|
||||
created by this builder.
|
||||
description: Optional human-readable description explaining the workflow's
|
||||
purpose. Useful for documentation and observability.
|
||||
checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence.
|
||||
termination_condition: Optional callable that receives the full conversation and returns True
|
||||
(or awaitable True) if the workflow should terminate.
|
||||
"""
|
||||
self._name = name
|
||||
self._description = description
|
||||
@@ -617,7 +622,7 @@ class HandoffBuilder:
|
||||
self._handoff_config: dict[str, set[HandoffConfiguration]] = {}
|
||||
|
||||
# Checkpoint related members
|
||||
self._checkpoint_storage: CheckpointStorage | None = None
|
||||
self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage
|
||||
|
||||
# Autonomous mode related
|
||||
self._autonomous_mode: bool = False
|
||||
@@ -626,7 +631,9 @@ class HandoffBuilder:
|
||||
self._autonomous_mode_enabled_agents: list[str] = []
|
||||
|
||||
# Termination related members
|
||||
self._termination_condition: Callable[[list[ChatMessage]], bool | Awaitable[bool]] | None = None
|
||||
self._termination_condition: Callable[[list[ChatMessage]], bool | Awaitable[bool]] | None = (
|
||||
termination_condition
|
||||
)
|
||||
|
||||
def register_participants(
|
||||
self, participant_factories: Mapping[str, Callable[[], SupportsAgentRun]]
|
||||
@@ -1060,7 +1067,9 @@ class HandoffBuilder:
|
||||
builder = WorkflowBuilder(
|
||||
name=self._name,
|
||||
description=self._description,
|
||||
).set_start_executor(start_executor)
|
||||
start_executor=start_executor,
|
||||
checkpoint_storage=self._checkpoint_storage,
|
||||
)
|
||||
|
||||
# Add the appropriate edges
|
||||
# In handoff workflows, all executors are connected, making a fully connected graph.
|
||||
@@ -1076,10 +1085,6 @@ class HandoffBuilder:
|
||||
elif len(targets) == 1:
|
||||
builder = builder.add_edge(executor, targets[0])
|
||||
|
||||
# Configure checkpointing if enabled
|
||||
if self._checkpoint_storage:
|
||||
builder.with_checkpointing(self._checkpoint_storage)
|
||||
|
||||
return builder.build()
|
||||
|
||||
# region Internal Helper Methods
|
||||
|
||||
@@ -10,7 +10,7 @@ from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, ClassVar, TypeVar, cast, overload
|
||||
from typing import Any, ClassVar, TypeVar, cast
|
||||
|
||||
from agent_framework import (
|
||||
AgentResponse,
|
||||
@@ -41,10 +41,6 @@ if sys.version_info >= (3, 12):
|
||||
from typing import override # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import override # type: ignore # pragma: no cover
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import Self # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import Self # type: ignore # pragma: no cover
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -1366,7 +1362,7 @@ class MagenticBuilder:
|
||||
Human-in-the-loop Support:
|
||||
Magentic provides specialized HITL mechanisms via:
|
||||
|
||||
- `.with_plan_review()` - Review and approve/revise plans before execution
|
||||
- `enable_plan_review=True` - Review and approve/revise plans before execution
|
||||
- `.with_human_input_on_stall()` - Intervene when workflow stalls
|
||||
- Tool approval via `function_approval_request` - Approve individual tool calls
|
||||
|
||||
@@ -1375,8 +1371,57 @@ class MagenticBuilder:
|
||||
for Magentic's planning-based orchestration.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the Magentic workflow builder."""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
participants: Sequence[SupportsAgentRun | Executor] | None = None,
|
||||
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]] | None = None,
|
||||
# Manager config (exactly one required)
|
||||
manager: MagenticManagerBase | None = None,
|
||||
manager_factory: Callable[[], MagenticManagerBase] | None = None,
|
||||
manager_agent: SupportsAgentRun | None = None,
|
||||
manager_agent_factory: Callable[[], SupportsAgentRun] | None = None,
|
||||
# StandardMagenticManager options (used with manager_agent/manager_agent_factory)
|
||||
task_ledger: _MagenticTaskLedger | None = None,
|
||||
task_ledger_facts_prompt: str | None = None,
|
||||
task_ledger_plan_prompt: str | None = None,
|
||||
task_ledger_full_prompt: str | None = None,
|
||||
task_ledger_facts_update_prompt: str | None = None,
|
||||
task_ledger_plan_update_prompt: str | None = None,
|
||||
progress_ledger_prompt: str | None = None,
|
||||
final_answer_prompt: str | None = None,
|
||||
max_stall_count: int = 3,
|
||||
max_reset_count: int | None = None,
|
||||
max_round_count: int | None = None,
|
||||
# Existing params
|
||||
enable_plan_review: bool = False,
|
||||
checkpoint_storage: CheckpointStorage | None = None,
|
||||
intermediate_outputs: bool = False,
|
||||
) -> None:
|
||||
"""Initialize the Magentic workflow builder.
|
||||
|
||||
Args:
|
||||
participants: Optional sequence of agent or executor instances for the workflow.
|
||||
participant_factories: Optional sequence of callables returning agent or executor instances.
|
||||
manager: Pre-configured manager instance (subclass of MagenticManagerBase).
|
||||
manager_factory: Callable that returns a new MagenticManagerBase instance.
|
||||
manager_agent: Agent instance for creating a StandardMagenticManager.
|
||||
manager_agent_factory: Callable that returns a new agent instance for creating a StandardMagenticManager.
|
||||
task_ledger: Optional custom task ledger (used with manager_agent/manager_agent_factory).
|
||||
task_ledger_facts_prompt: Custom prompt for extracting facts.
|
||||
task_ledger_plan_prompt: Custom prompt for generating initial plan.
|
||||
task_ledger_full_prompt: Custom prompt for complete task ledger.
|
||||
task_ledger_facts_update_prompt: Custom prompt for updating facts.
|
||||
task_ledger_plan_update_prompt: Custom prompt for replanning.
|
||||
progress_ledger_prompt: Custom prompt for assessing progress.
|
||||
final_answer_prompt: Custom prompt for synthesizing final response.
|
||||
max_stall_count: Max consecutive rounds without progress before replan (default 3).
|
||||
max_reset_count: Max number of resets allowed. None means unlimited.
|
||||
max_round_count: Max total coordination rounds. None means unlimited.
|
||||
enable_plan_review: If True, requires human approval of the initial plan before proceeding.
|
||||
checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence.
|
||||
intermediate_outputs: If True, enables intermediate outputs from agent participants.
|
||||
"""
|
||||
self._participants: dict[str, SupportsAgentRun | Executor] = {}
|
||||
self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = []
|
||||
|
||||
@@ -1385,78 +1430,64 @@ class MagenticBuilder:
|
||||
self._manager_factory: Callable[[], MagenticManagerBase] | None = None
|
||||
self._manager_agent_factory: Callable[[], SupportsAgentRun] | None = None
|
||||
self._standard_manager_options: dict[str, Any] = {}
|
||||
self._enable_plan_review: bool = False
|
||||
self._enable_plan_review: bool = enable_plan_review
|
||||
|
||||
self._checkpoint_storage: CheckpointStorage | None = None
|
||||
self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage
|
||||
|
||||
# Intermediate outputs
|
||||
self._intermediate_outputs = False
|
||||
self._intermediate_outputs = intermediate_outputs
|
||||
|
||||
def register_participants(
|
||||
if participants is None and participant_factories is None:
|
||||
raise ValueError("Either participants or participant_factories must be provided.")
|
||||
|
||||
if participant_factories is not None:
|
||||
self._set_participant_factories(participant_factories)
|
||||
if participants is not None:
|
||||
self._set_participants(participants)
|
||||
|
||||
# Set manager if provided
|
||||
if any(x is not None for x in [manager, manager_factory, manager_agent, manager_agent_factory]):
|
||||
self._set_manager(
|
||||
manager=manager,
|
||||
manager_factory=manager_factory,
|
||||
manager_agent=manager_agent,
|
||||
manager_agent_factory=manager_agent_factory,
|
||||
task_ledger=task_ledger,
|
||||
task_ledger_facts_prompt=task_ledger_facts_prompt,
|
||||
task_ledger_plan_prompt=task_ledger_plan_prompt,
|
||||
task_ledger_full_prompt=task_ledger_full_prompt,
|
||||
task_ledger_facts_update_prompt=task_ledger_facts_update_prompt,
|
||||
task_ledger_plan_update_prompt=task_ledger_plan_update_prompt,
|
||||
progress_ledger_prompt=progress_ledger_prompt,
|
||||
final_answer_prompt=final_answer_prompt,
|
||||
max_stall_count=max_stall_count,
|
||||
max_reset_count=max_reset_count,
|
||||
max_round_count=max_round_count,
|
||||
)
|
||||
|
||||
def _set_participant_factories(
|
||||
self,
|
||||
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]],
|
||||
) -> "MagenticBuilder":
|
||||
"""Register participant factories for this Magentic workflow.
|
||||
|
||||
Args:
|
||||
participant_factories: Sequence of callables that return SupportsAgentRun or Executor instances.
|
||||
|
||||
Returns:
|
||||
Self for method chaining
|
||||
|
||||
Raises:
|
||||
ValueError: If participant_factories is empty, or participants
|
||||
or participant factories are already set
|
||||
"""
|
||||
) -> None:
|
||||
"""Set participant factories (internal)."""
|
||||
if self._participants:
|
||||
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
|
||||
raise ValueError("Cannot provide both participants and participant_factories.")
|
||||
|
||||
if self._participant_factories:
|
||||
raise ValueError("register_participants() has already been called on this builder instance.")
|
||||
raise ValueError("participant_factories already set.")
|
||||
|
||||
if not participant_factories:
|
||||
raise ValueError("participant_factories cannot be empty")
|
||||
|
||||
self._participant_factories = list(participant_factories)
|
||||
return self
|
||||
|
||||
def participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> Self:
|
||||
"""Define participants for this Magentic workflow.
|
||||
|
||||
Accepts SupportsAgentRun instances (auto-wrapped as AgentExecutor) or Executor instances.
|
||||
|
||||
Args:
|
||||
participants: Sequence of participant definitions
|
||||
|
||||
Returns:
|
||||
Self for method chaining
|
||||
|
||||
Raises:
|
||||
ValueError: If participants are empty, names are duplicated, or participants
|
||||
or participant factories are already set
|
||||
TypeError: If any participant is not SupportsAgentRun or Executor instance
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants([research_agent, writing_agent, coding_agent, review_agent])
|
||||
.with_manager(agent=manager_agent)
|
||||
.build()
|
||||
)
|
||||
|
||||
Notes:
|
||||
- Participant names become part of the manager's context for selection
|
||||
- Agent descriptions (if available) are extracted and provided to the manager
|
||||
- Can be called multiple times to add participants incrementally
|
||||
"""
|
||||
def _set_participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> None:
|
||||
"""Set participants (internal)."""
|
||||
if self._participant_factories:
|
||||
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
|
||||
raise ValueError("Cannot provide both participants and participant_factories.")
|
||||
|
||||
if self._participants:
|
||||
raise ValueError("participants have already been set. Call participants(...) at most once.")
|
||||
raise ValueError("participants already set.")
|
||||
|
||||
if not participants:
|
||||
raise ValueError("participants cannot be empty.")
|
||||
@@ -1482,8 +1513,6 @@ class MagenticBuilder:
|
||||
|
||||
self._participants = named
|
||||
|
||||
return self
|
||||
|
||||
def with_plan_review(self, enable: bool = True) -> "MagenticBuilder":
|
||||
"""Enable or disable human-in-the-loop plan review before task execution.
|
||||
|
||||
@@ -1509,9 +1538,7 @@ class MagenticBuilder:
|
||||
.. code-block:: python
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants(agent1=agent1)
|
||||
.with_manager(agent=manager_agent)
|
||||
MagenticBuilder(participants=[agent1], manager_agent=manager_agent)
|
||||
.with_plan_review(enable=True)
|
||||
.build()
|
||||
)
|
||||
@@ -1556,11 +1583,7 @@ class MagenticBuilder:
|
||||
|
||||
storage = InMemoryCheckpointStorage()
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants([agent1])
|
||||
.with_manager(agent=manager_agent)
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
MagenticBuilder(participants=[agent1], manager_agent=manager_agent).with_checkpointing(storage).build()
|
||||
)
|
||||
|
||||
# First run
|
||||
@@ -1580,144 +1603,14 @@ class MagenticBuilder:
|
||||
self._checkpoint_storage = checkpoint_storage
|
||||
return self
|
||||
|
||||
@overload
|
||||
def with_manager(self, *, manager: MagenticManagerBase) -> Self:
|
||||
"""Configure the workflow with a pre-defined Magentic manager instance.
|
||||
|
||||
Args:
|
||||
manager: A custom manager instance (subclass of MagenticManagerBase)
|
||||
|
||||
Returns:
|
||||
Self for method chaining
|
||||
"""
|
||||
...
|
||||
|
||||
@overload
|
||||
def with_manager(self, *, manager_factory: Callable[[], MagenticManagerBase]) -> Self:
|
||||
"""Configure the workflow with a factory for creating custom Magentic manager instances.
|
||||
|
||||
Args:
|
||||
manager_factory: Callable that returns a new MagenticManagerBase instance
|
||||
|
||||
Returns:
|
||||
Self for method chaining
|
||||
"""
|
||||
...
|
||||
|
||||
@overload
|
||||
def with_manager(
|
||||
self,
|
||||
*,
|
||||
agent: SupportsAgentRun,
|
||||
task_ledger: _MagenticTaskLedger | None = None,
|
||||
# Prompt overrides
|
||||
task_ledger_facts_prompt: str | None = None,
|
||||
task_ledger_plan_prompt: str | None = None,
|
||||
task_ledger_full_prompt: str | None = None,
|
||||
task_ledger_facts_update_prompt: str | None = None,
|
||||
task_ledger_plan_update_prompt: str | None = None,
|
||||
progress_ledger_prompt: str | None = None,
|
||||
final_answer_prompt: str | None = None,
|
||||
# Limits
|
||||
max_stall_count: int = 3,
|
||||
max_reset_count: int | None = None,
|
||||
max_round_count: int | None = None,
|
||||
) -> Self:
|
||||
"""Configure the workflow with an agent for creating a standard manager.
|
||||
|
||||
This will create a StandardMagenticManager using the provided agent.
|
||||
|
||||
Args:
|
||||
agent: SupportsAgentRun instance for the standard magentic manager
|
||||
(`StandardMagenticManager`)
|
||||
task_ledger: Optional custom task ledger implementation for specialized
|
||||
prompting or structured output requirements
|
||||
task_ledger_facts_prompt: Custom prompt template for extracting facts from
|
||||
task description
|
||||
task_ledger_plan_prompt: Custom prompt template for generating initial plan
|
||||
task_ledger_full_prompt: Custom prompt template for complete task ledger
|
||||
(facts + plan combined)
|
||||
task_ledger_facts_update_prompt: Custom prompt template for updating facts
|
||||
based on agent progress
|
||||
task_ledger_plan_update_prompt: Custom prompt template for replanning when
|
||||
needed
|
||||
progress_ledger_prompt: Custom prompt template for assessing progress and
|
||||
determining next actions
|
||||
final_answer_prompt: Custom prompt template for synthesizing final response
|
||||
when task is complete
|
||||
max_stall_count: Maximum consecutive rounds without progress before triggering
|
||||
replan (default 3). Set to 0 to disable stall detection.
|
||||
max_reset_count: Maximum number of complete resets allowed before failing.
|
||||
None means unlimited resets.
|
||||
max_round_count: Maximum total coordination rounds before stopping with
|
||||
partial result. None means unlimited rounds.
|
||||
|
||||
Returns:
|
||||
Self for method chaining
|
||||
"""
|
||||
...
|
||||
|
||||
@overload
|
||||
def with_manager(
|
||||
self,
|
||||
*,
|
||||
agent_factory: Callable[[], SupportsAgentRun],
|
||||
task_ledger: _MagenticTaskLedger | None = None,
|
||||
# Prompt overrides
|
||||
task_ledger_facts_prompt: str | None = None,
|
||||
task_ledger_plan_prompt: str | None = None,
|
||||
task_ledger_full_prompt: str | None = None,
|
||||
task_ledger_facts_update_prompt: str | None = None,
|
||||
task_ledger_plan_update_prompt: str | None = None,
|
||||
progress_ledger_prompt: str | None = None,
|
||||
final_answer_prompt: str | None = None,
|
||||
# Limits
|
||||
max_stall_count: int = 3,
|
||||
max_reset_count: int | None = None,
|
||||
max_round_count: int | None = None,
|
||||
) -> Self:
|
||||
"""Configure the workflow with a factory for creating the manager agent.
|
||||
|
||||
This will create a StandardMagenticManager using the provided agent factory.
|
||||
|
||||
Args:
|
||||
agent_factory: Callable that returns a new SupportsAgentRun instance for the standard
|
||||
magentic manager (`StandardMagenticManager`)
|
||||
task_ledger: Optional custom task ledger implementation for specialized
|
||||
prompting or structured output requirements
|
||||
task_ledger_facts_prompt: Custom prompt template for extracting facts from
|
||||
task description
|
||||
task_ledger_plan_prompt: Custom prompt template for generating initial plan
|
||||
task_ledger_full_prompt: Custom prompt template for complete task ledger
|
||||
(facts + plan combined)
|
||||
task_ledger_facts_update_prompt: Custom prompt template for updating facts
|
||||
based on agent progress
|
||||
task_ledger_plan_update_prompt: Custom prompt template for replanning when
|
||||
needed
|
||||
progress_ledger_prompt: Custom prompt template for assessing progress and
|
||||
determining next actions
|
||||
final_answer_prompt: Custom prompt template for synthesizing final response
|
||||
when task is complete
|
||||
max_stall_count: Maximum consecutive rounds without progress before triggering
|
||||
replan (default 3). Set to 0 to disable stall detection.
|
||||
max_reset_count: Maximum number of complete resets allowed before failing.
|
||||
None means unlimited resets.
|
||||
max_round_count: Maximum total coordination rounds before stopping with
|
||||
partial result. None means unlimited rounds.
|
||||
|
||||
Returns:
|
||||
Self for method chaining
|
||||
"""
|
||||
...
|
||||
|
||||
def with_manager(
|
||||
def _set_manager(
|
||||
self,
|
||||
*,
|
||||
manager: MagenticManagerBase | None = None,
|
||||
manager_factory: Callable[[], MagenticManagerBase] | None = None,
|
||||
agent_factory: Callable[[], SupportsAgentRun] | None = None,
|
||||
manager_agent: SupportsAgentRun | None = None,
|
||||
manager_agent_factory: Callable[[], SupportsAgentRun] | None = None,
|
||||
# Constructor args for StandardMagenticManager when manager is not provided
|
||||
agent: SupportsAgentRun | None = None,
|
||||
task_ledger: _MagenticTaskLedger | None = None,
|
||||
# Prompt overrides
|
||||
task_ledger_facts_prompt: str | None = None,
|
||||
@@ -1731,123 +1624,37 @@ class MagenticBuilder:
|
||||
max_stall_count: int = 3,
|
||||
max_reset_count: int | None = None,
|
||||
max_round_count: int | None = None,
|
||||
) -> Self:
|
||||
"""Configure the workflow manager for task planning and agent coordination.
|
||||
|
||||
The manager is responsible for creating plans, selecting agents, tracking progress,
|
||||
and deciding when to replan or complete. This method supports four usage patterns:
|
||||
|
||||
1. **Provide existing manager**: Pass a pre-configured manager instance (custom
|
||||
or standard) for full control over behavior
|
||||
2. **Factory for custom manager**: Pass a callable that returns a new manager
|
||||
instance for more advanced scenarios so that the builder can be reused
|
||||
3. **Factory for agent**: Pass a callable that returns a new agent instance to
|
||||
automatically create a `StandardMagenticManager`
|
||||
4. **Auto-create with agent**: Pass an agent to automatically create a `StandardMagenticManager`
|
||||
) -> None:
|
||||
"""Configure the workflow manager for task planning and agent coordination (internal).
|
||||
|
||||
Args:
|
||||
manager: Pre-configured manager instance (`StandardMagenticManager` or custom
|
||||
`MagenticManagerBase` subclass). If provided, all other arguments are ignored.
|
||||
manager: Pre-configured manager instance.
|
||||
manager_factory: Callable that returns a new manager instance.
|
||||
agent_factory: Callable that returns a new agent instance.
|
||||
agent: Agent instance for generating plans and decisions. The agent's
|
||||
configured instructions and options (temperature, seed, etc.) will be
|
||||
applied.
|
||||
task_ledger: Optional custom task ledger implementation for specialized
|
||||
prompting or structured output requirements
|
||||
task_ledger_facts_prompt: Custom prompt template for extracting facts from
|
||||
task description
|
||||
task_ledger_plan_prompt: Custom prompt template for generating initial plan
|
||||
task_ledger_full_prompt: Custom prompt template for complete task ledger
|
||||
(facts + plan combined)
|
||||
task_ledger_facts_update_prompt: Custom prompt template for updating facts
|
||||
based on agent progress
|
||||
task_ledger_plan_update_prompt: Custom prompt template for replanning when
|
||||
needed
|
||||
progress_ledger_prompt: Custom prompt template for assessing progress and
|
||||
determining next actions
|
||||
final_answer_prompt: Custom prompt template for synthesizing final response
|
||||
when task is complete
|
||||
max_stall_count: Maximum consecutive rounds without progress before triggering
|
||||
replan (default 3). Set to 0 to disable stall detection.
|
||||
max_reset_count: Maximum number of complete resets allowed before failing.
|
||||
None means unlimited resets.
|
||||
max_round_count: Maximum total coordination rounds before stopping with
|
||||
partial result. None means unlimited rounds.
|
||||
|
||||
Returns:
|
||||
Self for method chaining
|
||||
manager_agent: Agent instance for creating a StandardMagenticManager.
|
||||
manager_agent_factory: Callable that returns a new agent instance for creating a StandardMagenticManager.
|
||||
task_ledger: Optional custom task ledger implementation.
|
||||
task_ledger_facts_prompt: Custom prompt for extracting facts.
|
||||
task_ledger_plan_prompt: Custom prompt for generating initial plan.
|
||||
task_ledger_full_prompt: Custom prompt for complete task ledger.
|
||||
task_ledger_facts_update_prompt: Custom prompt for updating facts.
|
||||
task_ledger_plan_update_prompt: Custom prompt for replanning.
|
||||
progress_ledger_prompt: Custom prompt for assessing progress.
|
||||
final_answer_prompt: Custom prompt for synthesizing final response.
|
||||
max_stall_count: Max consecutive rounds without progress before replan (default 3).
|
||||
max_reset_count: Max number of resets allowed. None means unlimited.
|
||||
max_round_count: Max total coordination rounds. None means unlimited.
|
||||
|
||||
Raises:
|
||||
ValueError: If manager is None and agent is not provided.
|
||||
|
||||
Usage with agent (recommended):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ChatAgent, ChatOptions
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
# Configure manager agent with specific options and instructions
|
||||
manager_agent = ChatAgent(
|
||||
name="Coordinator",
|
||||
chat_client=OpenAIChatClient(model_id="gpt-4o"),
|
||||
options=ChatOptions(temperature=0.3, seed=42),
|
||||
instructions="Be concise and focus on accuracy",
|
||||
)
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants(agent1=agent1, agent2=agent2)
|
||||
.with_manager(
|
||||
agent=manager_agent,
|
||||
max_round_count=20,
|
||||
max_stall_count=3,
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
Usage with custom manager:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyManager(MagenticManagerBase):
|
||||
async def plan(self, context: MagenticContext) -> ChatMessage:
|
||||
# Custom planning logic
|
||||
return ChatMessage(role="assistant", text="...")
|
||||
|
||||
|
||||
manager = MyManager()
|
||||
workflow = MagenticBuilder().participants(agent1=agent1).with_manager(manager).build()
|
||||
|
||||
Usage with prompt customization:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants(coder=coder_agent, reviewer=reviewer_agent)
|
||||
.with_manager(
|
||||
agent=manager_agent,
|
||||
task_ledger_plan_prompt="Create a detailed step-by-step plan...",
|
||||
progress_ledger_prompt="Assess progress and decide next action...",
|
||||
max_stall_count=2,
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
Notes:
|
||||
- StandardMagenticManager uses structured LLM calls for all decisions
|
||||
- Custom managers can implement alternative selection strategies
|
||||
- Prompt templates support Jinja2-style variable substitution
|
||||
- Stall detection helps prevent infinite loops in stuck scenarios
|
||||
- The agent's instructions are used as system instructions for all manager prompts
|
||||
ValueError: If a manager has already been set or if none or multiple
|
||||
of the primary parameters are provided.
|
||||
"""
|
||||
if any([self._manager, self._manager_factory, self._manager_agent_factory]):
|
||||
raise ValueError("with_manager() has already been called on this builder instance.")
|
||||
raise ValueError("Manager has already been configured. Set manager config once only.")
|
||||
|
||||
if sum(x is not None for x in [manager, agent, manager_factory, agent_factory]) != 1:
|
||||
raise ValueError("Exactly one of manager, agent, manager_factory, or agent_factory must be provided.")
|
||||
if sum(x is not None for x in [manager, manager_agent, manager_factory, manager_agent_factory]) != 1:
|
||||
raise ValueError(
|
||||
"Exactly one of manager, manager_agent, manager_factory, or manager_agent_factory must be provided."
|
||||
)
|
||||
|
||||
def _log_warning_if_constructor_args_provided() -> None:
|
||||
if any(
|
||||
@@ -1866,14 +1673,14 @@ class MagenticBuilder:
|
||||
max_round_count,
|
||||
]
|
||||
):
|
||||
logger.warning("Customer manager provided; all other with_manager() arguments will be ignored.")
|
||||
logger.warning("Custom manager provided; all other manager arguments will be ignored.")
|
||||
|
||||
if manager is not None:
|
||||
self._manager = manager
|
||||
_log_warning_if_constructor_args_provided()
|
||||
elif agent is not None:
|
||||
elif manager_agent is not None:
|
||||
self._manager = StandardMagenticManager(
|
||||
agent=agent,
|
||||
agent=manager_agent,
|
||||
task_ledger=task_ledger,
|
||||
task_ledger_facts_prompt=task_ledger_facts_prompt,
|
||||
task_ledger_plan_prompt=task_ledger_plan_prompt,
|
||||
@@ -1889,8 +1696,8 @@ class MagenticBuilder:
|
||||
elif manager_factory is not None:
|
||||
self._manager_factory = manager_factory
|
||||
_log_warning_if_constructor_args_provided()
|
||||
elif agent_factory is not None:
|
||||
self._manager_agent_factory = agent_factory
|
||||
elif manager_agent_factory is not None:
|
||||
self._manager_agent_factory = manager_agent_factory
|
||||
self._standard_manager_options = {
|
||||
"task_ledger": task_ledger,
|
||||
"task_ledger_facts_prompt": task_ledger_facts_prompt,
|
||||
@@ -1905,21 +1712,6 @@ class MagenticBuilder:
|
||||
"max_round_count": max_round_count,
|
||||
}
|
||||
|
||||
return self
|
||||
|
||||
def with_intermediate_outputs(self) -> Self:
|
||||
"""Enable intermediate outputs from agent participants before aggregation.
|
||||
|
||||
When enabled, the workflow returns each agent participant's response or yields
|
||||
streaming updates as they become available. The output of the orchestrator will
|
||||
always be available as the final output of the workflow.
|
||||
|
||||
Returns:
|
||||
Self for fluent chaining
|
||||
"""
|
||||
self._intermediate_outputs = True
|
||||
return self
|
||||
|
||||
def _resolve_orchestrator(self, participants: Sequence[Executor]) -> Executor:
|
||||
"""Determine the orchestrator to use for the workflow.
|
||||
|
||||
@@ -1927,8 +1719,11 @@ class MagenticBuilder:
|
||||
participants: List of resolved participant executors
|
||||
"""
|
||||
if all(x is None for x in [self._manager, self._manager_factory, self._manager_agent_factory]):
|
||||
raise ValueError("No manager configured. Call with_manager(...) before building the orchestrator.")
|
||||
# We don't need to check if multiple are set since that is handled in with_orchestrator()
|
||||
raise ValueError(
|
||||
"No manager configured. "
|
||||
"Pass manager, manager_factory, manager_agent, or manager_agent_factory to the constructor."
|
||||
)
|
||||
# We don't need to check if multiple are set since that is handled in _set_manager()
|
||||
|
||||
if self._manager:
|
||||
manager = self._manager
|
||||
@@ -1942,7 +1737,10 @@ class MagenticBuilder:
|
||||
)
|
||||
else:
|
||||
# This should never be reached due to the checks above
|
||||
raise RuntimeError("Manager could not be resolved. Please set the manager properly with with_manager().")
|
||||
raise RuntimeError(
|
||||
"Manager could not be resolved. "
|
||||
"Pass manager, manager_factory, manager_agent, or manager_agent_factory to the constructor."
|
||||
)
|
||||
|
||||
return MagenticOrchestrator(
|
||||
manager=manager,
|
||||
@@ -1953,7 +1751,7 @@ class MagenticBuilder:
|
||||
def _resolve_participants(self) -> list[Executor]:
|
||||
"""Resolve participant instances into Executor objects."""
|
||||
if not self._participants and not self._participant_factories:
|
||||
raise ValueError("No participants provided. Call .participants() or .register_participants() first.")
|
||||
raise ValueError("No participants provided. Pass participants or participant_factories to the constructor.")
|
||||
# We don't need to check if both are set since that is handled in the respective methods
|
||||
|
||||
participants: list[Executor | SupportsAgentRun] = []
|
||||
@@ -1985,17 +1783,15 @@ class MagenticBuilder:
|
||||
orchestrator: Executor = self._resolve_orchestrator(participants)
|
||||
|
||||
# Build workflow graph
|
||||
workflow_builder = WorkflowBuilder().set_start_executor(orchestrator)
|
||||
workflow_builder = WorkflowBuilder(
|
||||
start_executor=orchestrator,
|
||||
checkpoint_storage=self._checkpoint_storage,
|
||||
output_executors=[orchestrator] if not self._intermediate_outputs else None,
|
||||
)
|
||||
for participant in participants:
|
||||
# Orchestrator and participant bi-directional edges
|
||||
workflow_builder = workflow_builder.add_edge(orchestrator, participant)
|
||||
workflow_builder = workflow_builder.add_edge(participant, orchestrator)
|
||||
if self._checkpoint_storage is not None:
|
||||
workflow_builder = workflow_builder.with_checkpointing(self._checkpoint_storage)
|
||||
|
||||
if not self._intermediate_outputs:
|
||||
# Constrain output to orchestrator only
|
||||
workflow_builder = workflow_builder.with_output_from([orchestrator])
|
||||
|
||||
return workflow_builder.build()
|
||||
|
||||
|
||||
+1
-2
@@ -132,11 +132,10 @@ class AgentApprovalExecutor(WorkflowExecutor):
|
||||
request_info_executor = AgentRequestInfoExecutor(id="agent_request_info_executor")
|
||||
|
||||
return (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=agent_executor)
|
||||
# Create a loop between agent executor and request info executor
|
||||
.add_edge(agent_executor, request_info_executor)
|
||||
.add_edge(request_info_executor, agent_executor)
|
||||
.set_start_executor(agent_executor)
|
||||
.build()
|
||||
)
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
This module provides a high-level, agent-focused API to assemble a sequential
|
||||
workflow where:
|
||||
- Participants can be provided as SupportsAgentRun or Executor instances via `.participants()`,
|
||||
or as factories returning SupportsAgentRun or Executor via `.register_participants()`
|
||||
- Participants can be provided as SupportsAgentRun or Executor instances via `participants=[...]`,
|
||||
or as factories returning SupportsAgentRun or Executor via `participant_factories=[...]`
|
||||
- A shared conversation context (list[ChatMessage]) is passed along the chain
|
||||
- Agents append their assistant messages to the context
|
||||
- Custom executors can transform or summarize and return a refined context
|
||||
@@ -109,8 +109,8 @@ class _EndWithConversation(Executor):
|
||||
class SequentialBuilder:
|
||||
r"""High-level builder for sequential agent/executor workflows with shared context.
|
||||
|
||||
- `participants([...])` accepts a list of SupportsAgentRun (recommended) or Executor instances
|
||||
- `register_participants([...])` accepts a list of factories for SupportsAgentRun (recommended)
|
||||
- `participants=[...]` accepts a list of SupportsAgentRun (recommended) or Executor instances
|
||||
- `participant_factories=[...]` accepts a list of factories for SupportsAgentRun (recommended)
|
||||
or Executor factories
|
||||
- Executors must define a handler that consumes list[ChatMessage] and sends out a list[ChatMessage]
|
||||
- The workflow wires participants in order, passing a list[ChatMessage] down the chain
|
||||
@@ -125,64 +125,81 @@ class SequentialBuilder:
|
||||
from agent_framework_orchestrations import SequentialBuilder
|
||||
|
||||
# With agent instances
|
||||
workflow = SequentialBuilder().participants([agent1, agent2, summarizer_exec]).build()
|
||||
workflow = SequentialBuilder(participants=[agent1, agent2, summarizer_exec]).build()
|
||||
|
||||
# With agent factories
|
||||
workflow = (
|
||||
SequentialBuilder().register_participants([create_agent1, create_agent2, create_summarizer_exec]).build()
|
||||
)
|
||||
workflow = SequentialBuilder(
|
||||
participant_factories=[create_agent1, create_agent2, create_summarizer_exec]
|
||||
).build()
|
||||
|
||||
# Enable checkpoint persistence
|
||||
workflow = SequentialBuilder().participants([agent1, agent2]).with_checkpointing(storage).build()
|
||||
workflow = SequentialBuilder(participants=[agent1, agent2], checkpoint_storage=storage).build()
|
||||
|
||||
# Enable request info for mid-workflow feedback (pauses before each agent)
|
||||
workflow = SequentialBuilder().participants([agent1, agent2]).with_request_info().build()
|
||||
workflow = SequentialBuilder(participants=[agent1, agent2]).with_request_info().build()
|
||||
|
||||
# Enable request info only for specific agents
|
||||
workflow = (
|
||||
SequentialBuilder()
|
||||
.participants([agent1, agent2, agent3])
|
||||
SequentialBuilder(participants=[agent1, agent2, agent3])
|
||||
.with_request_info(agents=[agent2]) # Only pause before agent2
|
||||
.build()
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
participants: Sequence[SupportsAgentRun | Executor] | None = None,
|
||||
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]] | None = None,
|
||||
checkpoint_storage: CheckpointStorage | None = None,
|
||||
intermediate_outputs: bool = False,
|
||||
) -> None:
|
||||
"""Initialize the SequentialBuilder.
|
||||
|
||||
Args:
|
||||
participants: Optional sequence of agent or executor instances to run sequentially.
|
||||
participant_factories: Optional sequence of callables returning agent or executor instances.
|
||||
checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence.
|
||||
intermediate_outputs: If True, enables intermediate outputs from agent participants.
|
||||
"""
|
||||
self._participants: list[SupportsAgentRun | Executor] = []
|
||||
self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = []
|
||||
self._checkpoint_storage: CheckpointStorage | None = None
|
||||
self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage
|
||||
self._request_info_enabled: bool = False
|
||||
self._request_info_filter: set[str] | None = None
|
||||
self._intermediate_outputs: bool = False
|
||||
self._intermediate_outputs: bool = intermediate_outputs
|
||||
|
||||
def register_participants(
|
||||
if participants is None and participant_factories is None:
|
||||
raise ValueError("Either participants or participant_factories must be provided.")
|
||||
|
||||
if participant_factories is not None:
|
||||
self._set_participant_factories(participant_factories)
|
||||
if participants is not None:
|
||||
self._set_participants(participants)
|
||||
|
||||
def _set_participant_factories(
|
||||
self,
|
||||
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]],
|
||||
) -> "SequentialBuilder":
|
||||
"""Register participant factories for this sequential workflow."""
|
||||
) -> None:
|
||||
"""Set participant factories (internal)."""
|
||||
if self._participants:
|
||||
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
|
||||
raise ValueError("Cannot provide both participants and participant_factories.")
|
||||
|
||||
if self._participant_factories:
|
||||
raise ValueError("register_participants() has already been called on this builder instance.")
|
||||
raise ValueError("participant_factories already set.")
|
||||
|
||||
if not participant_factories:
|
||||
raise ValueError("participant_factories cannot be empty")
|
||||
|
||||
self._participant_factories = list(participant_factories)
|
||||
return self
|
||||
|
||||
def participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> "SequentialBuilder":
|
||||
"""Define the ordered participants for this sequential workflow.
|
||||
|
||||
Accepts SupportsAgentRun instances (auto-wrapped as AgentExecutor) or Executor instances.
|
||||
Raises if empty or duplicates are provided for clarity.
|
||||
"""
|
||||
def _set_participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> None:
|
||||
"""Set participants (internal)."""
|
||||
if self._participant_factories:
|
||||
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
|
||||
raise ValueError("Cannot provide both participants and participant_factories.")
|
||||
|
||||
if self._participants:
|
||||
raise ValueError("participants() has already been called on this builder instance.")
|
||||
raise ValueError("participants already set.")
|
||||
|
||||
if not participants:
|
||||
raise ValueError("participants cannot be empty")
|
||||
@@ -203,12 +220,6 @@ class SequentialBuilder:
|
||||
seen_agent_ids.add(pid)
|
||||
|
||||
self._participants = list(participants)
|
||||
return self
|
||||
|
||||
def with_checkpointing(self, checkpoint_storage: CheckpointStorage) -> "SequentialBuilder":
|
||||
"""Enable checkpointing for the built workflow using the provided storage."""
|
||||
self._checkpoint_storage = checkpoint_storage
|
||||
return self
|
||||
|
||||
def with_request_info(
|
||||
self,
|
||||
@@ -243,23 +254,10 @@ class SequentialBuilder:
|
||||
|
||||
return self
|
||||
|
||||
def with_intermediate_outputs(self) -> "SequentialBuilder":
|
||||
"""Enable intermediate outputs from agent participants.
|
||||
|
||||
When enabled, the workflow returns each agent participant's response or yields
|
||||
streaming updates as they become available. The output of the last participant
|
||||
will always be available as the final output of the workflow.
|
||||
|
||||
Returns:
|
||||
Self for fluent chaining
|
||||
"""
|
||||
self._intermediate_outputs = True
|
||||
return self
|
||||
|
||||
def _resolve_participants(self) -> list[Executor]:
|
||||
"""Resolve participant instances into Executor objects."""
|
||||
if not self._participants and not self._participant_factories:
|
||||
raise ValueError("No participants provided. Call .participants() or .register_participants() first.")
|
||||
raise ValueError("No participants provided. Pass participants or participant_factories to the constructor.")
|
||||
# We don't need to check if both are set since that is handled in the respective methods
|
||||
|
||||
participants: list[Executor | SupportsAgentRun] = []
|
||||
@@ -308,8 +306,11 @@ class SequentialBuilder:
|
||||
# Resolve participants and participant factories to executors
|
||||
participants: list[Executor] = self._resolve_participants()
|
||||
|
||||
builder = WorkflowBuilder()
|
||||
builder.set_start_executor(input_conv)
|
||||
builder = WorkflowBuilder(
|
||||
start_executor=input_conv,
|
||||
checkpoint_storage=self._checkpoint_storage,
|
||||
output_executors=[end] if not self._intermediate_outputs else None,
|
||||
)
|
||||
|
||||
# Start of the chain is the input normalizer
|
||||
prior: Executor | SupportsAgentRun = input_conv
|
||||
@@ -319,11 +320,4 @@ class SequentialBuilder:
|
||||
# Terminate with the final conversation
|
||||
builder.add_edge(prior, end)
|
||||
|
||||
if not self._intermediate_outputs:
|
||||
# Constrain output to end only
|
||||
builder = builder.with_output_from([end])
|
||||
|
||||
if self._checkpoint_storage is not None:
|
||||
builder = builder.with_checkpointing(self._checkpoint_storage)
|
||||
|
||||
return builder.build()
|
||||
|
||||
@@ -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