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
@@ -152,7 +152,7 @@ class Workflow(DictConvertible):
|
||||
Checkpointing can be configured at build time or runtime:
|
||||
|
||||
Build-time (via WorkflowBuilder):
|
||||
workflow = WorkflowBuilder().with_checkpointing(storage).build()
|
||||
workflow = WorkflowBuilder(checkpoint_storage=storage).build()
|
||||
|
||||
Runtime (via run parameters):
|
||||
result = await workflow.run(message, checkpoint_storage=runtime_storage)
|
||||
@@ -428,7 +428,7 @@ class Workflow(DictConvertible):
|
||||
if not has_checkpointing and checkpoint_storage is None:
|
||||
raise ValueError(
|
||||
"Cannot restore from checkpoint: either provide checkpoint_storage parameter "
|
||||
"or build workflow with WorkflowBuilder.with_checkpointing(checkpoint_storage)."
|
||||
"or build workflow with WorkflowBuilder(checkpoint_storage=checkpoint_storage)."
|
||||
)
|
||||
|
||||
await self._runner.restore_from_checkpoint(checkpoint_id, checkpoint_storage)
|
||||
|
||||
@@ -138,11 +138,10 @@ class WorkflowBuilder:
|
||||
|
||||
# Build a workflow
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor="UpperCase")
|
||||
.register_executor(lambda: UpperCaseExecutor(id="upper"), name="UpperCase")
|
||||
.register_executor(lambda: ReverseExecutor(id="reverse"), name="Reverse")
|
||||
.add_edge("UpperCase", "Reverse")
|
||||
.set_start_executor("UpperCase")
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -156,23 +155,32 @@ class WorkflowBuilder:
|
||||
max_iterations: int = DEFAULT_MAX_ITERATIONS,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
*,
|
||||
start_executor: Executor | SupportsAgentRun | str,
|
||||
checkpoint_storage: CheckpointStorage | None = None,
|
||||
output_executors: list[Executor | SupportsAgentRun | str] | None = None,
|
||||
):
|
||||
"""Initialize the WorkflowBuilder with an empty list of edges and no starting executor.
|
||||
"""Initialize the WorkflowBuilder.
|
||||
|
||||
Args:
|
||||
max_iterations: Maximum number of iterations for workflow convergence. Default is 100.
|
||||
name: Optional human-readable name for the workflow.
|
||||
description: Optional description of what the workflow does.
|
||||
start_executor: The starting executor for the workflow. Can be an Executor instance,
|
||||
SupportsAgentRun instance, or the name of a registered executor factory.
|
||||
checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence.
|
||||
output_executors: Optional list of executors whose outputs should be collected.
|
||||
If not provided, outputs from all executors are collected.
|
||||
"""
|
||||
self._edge_groups: list[EdgeGroup] = []
|
||||
self._executors: dict[str, Executor] = {}
|
||||
self._start_executor: Executor | str | None = None
|
||||
self._checkpoint_storage: CheckpointStorage | None = None
|
||||
self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage
|
||||
self._max_iterations: int = max_iterations
|
||||
self._name: str | None = name
|
||||
self._description: str | None = description
|
||||
# Maps underlying SupportsAgentRun object id -> wrapped Executor so we reuse the same wrapper
|
||||
# across set_start_executor / add_edge calls. This avoids multiple AgentExecutor instances
|
||||
# across start_executor / add_edge calls. This avoids multiple AgentExecutor instances
|
||||
# being created for the same agent.
|
||||
self._agent_wrappers: dict[str, Executor] = {}
|
||||
|
||||
@@ -187,7 +195,10 @@ class WorkflowBuilder:
|
||||
self._executor_registry: dict[str, Callable[[], Executor]] = {}
|
||||
|
||||
# Output executors filter; if set, only outputs from these executors are yielded
|
||||
self._output_executors: list[Executor | SupportsAgentRun | str] = []
|
||||
self._output_executors: list[Executor | SupportsAgentRun | str] = output_executors if output_executors else []
|
||||
|
||||
# Set the start executor
|
||||
self._set_start_executor(start_executor)
|
||||
|
||||
# Agents auto-wrapped by builder now always stream incremental updates.
|
||||
|
||||
@@ -279,10 +290,9 @@ class WorkflowBuilder:
|
||||
|
||||
# Build a workflow
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor="UpperCase")
|
||||
.register_executor(lambda: UpperCaseExecutor(id="upper"), name="UpperCase")
|
||||
.register_executor(lambda: ReverseExecutor(id="reverse"), name="Reverse")
|
||||
.set_start_executor("UpperCase")
|
||||
.add_edge("UpperCase", "Reverse")
|
||||
.build()
|
||||
)
|
||||
@@ -302,9 +312,8 @@ class WorkflowBuilder:
|
||||
|
||||
# Register the same executor factory under multiple names
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor="ExecutorA")
|
||||
.register_executor(lambda: LoggerExecutor(id="logger"), name=["ExecutorA", "ExecutorB"])
|
||||
.set_start_executor("ExecutorA")
|
||||
.add_edge("ExecutorA", "ExecutorB")
|
||||
.build()
|
||||
"""
|
||||
@@ -347,7 +356,7 @@ class WorkflowBuilder:
|
||||
|
||||
# Build a workflow
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor="SomeOtherExecutor")
|
||||
.register_executor(lambda: ..., name="SomeOtherExecutor")
|
||||
.register_agent(
|
||||
lambda: AnthropicAgent(name="writer", model="claude-3-5-sonnet-20241022"),
|
||||
@@ -355,7 +364,6 @@ class WorkflowBuilder:
|
||||
output_response=True,
|
||||
)
|
||||
.add_edge("SomeOtherExecutor", "WriterAgent")
|
||||
.set_start_executor("SomeOtherExecutor")
|
||||
.build()
|
||||
)
|
||||
"""
|
||||
@@ -420,20 +428,18 @@ class WorkflowBuilder:
|
||||
|
||||
# Connect executors with an edge
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor="ProcessorA")
|
||||
.register_executor(lambda: ProcessorA(id="a"), name="ProcessorA")
|
||||
.register_executor(lambda: ProcessorB(id="b"), name="ProcessorB")
|
||||
.add_edge("ProcessorA", "ProcessorB")
|
||||
.set_start_executor("ProcessorA")
|
||||
.build()
|
||||
)
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor="ProcessorA")
|
||||
.register_executor(lambda: ProcessorA(id="a"), name="ProcessorA")
|
||||
.register_executor(lambda: ProcessorB(id="b"), name="ProcessorB")
|
||||
.add_edge("ProcessorA", "ProcessorB", condition=only_large_numbers)
|
||||
.set_start_executor("ProcessorA")
|
||||
.build()
|
||||
)
|
||||
"""
|
||||
@@ -507,12 +513,11 @@ class WorkflowBuilder:
|
||||
|
||||
# Broadcast to multiple validators
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor="DataSource")
|
||||
.register_executor(lambda: DataSource(id="source"), name="DataSource")
|
||||
.register_executor(lambda: ValidatorA(id="val_a"), name="ValidatorA")
|
||||
.register_executor(lambda: ValidatorB(id="val_b"), name="ValidatorB")
|
||||
.add_fan_out_edges("DataSource", ["ValidatorA", "ValidatorB"])
|
||||
.set_start_executor("DataSource")
|
||||
.build()
|
||||
)
|
||||
"""
|
||||
@@ -600,7 +605,7 @@ class WorkflowBuilder:
|
||||
|
||||
# Route based on score value
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor="Evaluator")
|
||||
.register_executor(lambda: Evaluator(id="eval"), name="Evaluator")
|
||||
.register_executor(lambda: HighScoreHandler(id="high"), name="HighScoreHandler")
|
||||
.register_executor(lambda: LowScoreHandler(id="low"), name="LowScoreHandler")
|
||||
@@ -611,7 +616,6 @@ class WorkflowBuilder:
|
||||
Default(target="LowScoreHandler"),
|
||||
],
|
||||
)
|
||||
.set_start_executor("Evaluator")
|
||||
.build()
|
||||
)
|
||||
"""
|
||||
@@ -714,7 +718,7 @@ class WorkflowBuilder:
|
||||
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor="TaskDispatcher")
|
||||
.register_executor(lambda: TaskDispatcher(id="dispatcher"), name="TaskDispatcher")
|
||||
.register_executor(lambda: WorkerA(id="worker_a"), name="WorkerA")
|
||||
.register_executor(lambda: WorkerB(id="worker_b"), name="WorkerB")
|
||||
@@ -723,7 +727,6 @@ class WorkflowBuilder:
|
||||
["WorkerA", "WorkerB"],
|
||||
selection_func=select_workers,
|
||||
)
|
||||
.set_start_executor("TaskDispatcher")
|
||||
.build()
|
||||
)
|
||||
"""
|
||||
@@ -803,12 +806,11 @@ class WorkflowBuilder:
|
||||
|
||||
# Collect results from multiple producers
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor="Producer1")
|
||||
.register_executor(lambda: Producer(id="prod_1"), name="Producer1")
|
||||
.register_executor(lambda: Producer(id="prod_2"), name="Producer2")
|
||||
.register_executor(lambda: Aggregator(id="agg"), name="Aggregator")
|
||||
.add_fan_in_edges(["Producer1", "Producer2"], "Aggregator")
|
||||
.set_start_executor("Producer1")
|
||||
.build()
|
||||
)
|
||||
"""
|
||||
@@ -880,12 +882,11 @@ class WorkflowBuilder:
|
||||
|
||||
# Chain executors in sequence
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor="step1")
|
||||
.register_executor(lambda: Step1(id="step1"), name="step1")
|
||||
.register_executor(lambda: Step2(id="step2"), name="step2")
|
||||
.register_executor(lambda: Step3(id="step3"), name="step3")
|
||||
.add_chain(["step1", "step2", "step3"])
|
||||
.set_start_executor("step1")
|
||||
.build()
|
||||
)
|
||||
"""
|
||||
@@ -911,46 +912,12 @@ class WorkflowBuilder:
|
||||
self.add_edge(wrapped[i], wrapped[i + 1])
|
||||
return self
|
||||
|
||||
def set_start_executor(self, executor: Executor | SupportsAgentRun | str) -> Self:
|
||||
"""Set the starting executor for the workflow.
|
||||
|
||||
The start executor is the entry point for the workflow. When the workflow is executed,
|
||||
the initial message will be sent to this executor.
|
||||
def _set_start_executor(self, executor: Executor | SupportsAgentRun | str) -> None:
|
||||
"""Set the starting executor for the workflow (internal method).
|
||||
|
||||
Args:
|
||||
executor: The starting executor, which can be an Executor instance, SupportsAgentRun instance,
|
||||
or the name of a registered executor factory.
|
||||
|
||||
Returns:
|
||||
Self: The WorkflowBuilder instance for method chaining.
|
||||
|
||||
Example:
|
||||
.. code-block:: python
|
||||
|
||||
from typing_extensions import Never
|
||||
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
|
||||
|
||||
|
||||
class EntryPoint(Executor):
|
||||
@handler
|
||||
async def process(self, text: str, ctx: WorkflowContext[str]) -> None:
|
||||
await ctx.send_message(text.upper())
|
||||
|
||||
|
||||
class Processor(Executor):
|
||||
@handler
|
||||
async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
|
||||
await ctx.yield_output(text)
|
||||
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.register_executor(lambda: EntryPoint(id="entry"), name="EntryPoint")
|
||||
.register_executor(lambda: Processor(id="proc"), name="Processor")
|
||||
.add_edge("EntryPoint", "Processor")
|
||||
.set_start_executor("EntryPoint")
|
||||
.build()
|
||||
)
|
||||
"""
|
||||
if self._start_executor is not None:
|
||||
start_id = self._start_executor if isinstance(self._start_executor, str) else self._start_executor.id
|
||||
@@ -966,123 +933,9 @@ class WorkflowBuilder:
|
||||
existing = self._executors.get(wrapped.id)
|
||||
if existing is not wrapped:
|
||||
self._add_executor(wrapped)
|
||||
return self
|
||||
|
||||
def set_max_iterations(self, max_iterations: int) -> Self:
|
||||
"""Set the maximum number of iterations for the workflow.
|
||||
|
||||
When a workflow contains cycles, this limit prevents infinite loops by capping
|
||||
the total number of executor invocations. The default is 100 iterations.
|
||||
|
||||
Args:
|
||||
max_iterations: The maximum number of iterations the workflow will run for convergence.
|
||||
|
||||
Returns:
|
||||
Self: The WorkflowBuilder instance for method chaining.
|
||||
|
||||
Example:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
|
||||
|
||||
|
||||
class StepA(Executor):
|
||||
@handler
|
||||
async def process(self, count: int, ctx: WorkflowContext[int]) -> None:
|
||||
if count < 10:
|
||||
await ctx.send_message(count + 1)
|
||||
|
||||
|
||||
class StepB(Executor):
|
||||
@handler
|
||||
async def process(self, count: int, ctx: WorkflowContext[int]) -> None:
|
||||
await ctx.send_message(count)
|
||||
|
||||
|
||||
# Set a custom iteration limit for workflow with cycles
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_max_iterations(500)
|
||||
.register_executor(lambda: StepA(id="step_a"), name="StepA")
|
||||
.register_executor(lambda: StepB(id="step_b"), name="StepB")
|
||||
.add_edge("StepA", "StepB")
|
||||
.add_edge("StepB", "StepA") # Cycle
|
||||
.set_start_executor("StepA")
|
||||
.build()
|
||||
)
|
||||
"""
|
||||
self._max_iterations = max_iterations
|
||||
return self
|
||||
|
||||
# Removed explicit set_agent_streaming() API; agents always stream updates.
|
||||
|
||||
def with_checkpointing(self, checkpoint_storage: CheckpointStorage) -> Self:
|
||||
"""Enable checkpointing with the specified storage.
|
||||
|
||||
Checkpointing allows workflows to save their state periodically, enabling
|
||||
pause/resume functionality and recovery from failures. The checkpoint storage
|
||||
implementation determines where checkpoints are persisted.
|
||||
|
||||
Args:
|
||||
checkpoint_storage: The checkpoint storage implementation to use.
|
||||
|
||||
Returns:
|
||||
Self: The WorkflowBuilder instance for method chaining.
|
||||
|
||||
Example:
|
||||
.. code-block:: python
|
||||
|
||||
from typing_extensions import Never
|
||||
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
|
||||
from agent_framework import FileCheckpointStorage
|
||||
|
||||
|
||||
class ProcessorA(Executor):
|
||||
@handler
|
||||
async def process(self, text: str, ctx: WorkflowContext[str]) -> None:
|
||||
await ctx.send_message(text.upper())
|
||||
|
||||
|
||||
class ProcessorB(Executor):
|
||||
@handler
|
||||
async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
|
||||
await ctx.yield_output(text)
|
||||
|
||||
|
||||
# Enable checkpointing with file-based storage
|
||||
storage = FileCheckpointStorage("./checkpoints")
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.register_executor(lambda: ProcessorA(id="proc_a"), name="ProcessorA")
|
||||
.register_executor(lambda: ProcessorB(id="proc_b"), name="ProcessorB")
|
||||
.add_edge("ProcessorA", "ProcessorB")
|
||||
.set_start_executor("ProcessorA")
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
|
||||
# Run with checkpoint saving
|
||||
events = await workflow.run("input")
|
||||
"""
|
||||
self._checkpoint_storage = checkpoint_storage
|
||||
return self
|
||||
|
||||
def with_output_from(self, executors: list[Executor | SupportsAgentRun | str]) -> Self:
|
||||
"""Specify which executors' outputs should be collected as workflow outputs.
|
||||
|
||||
By default, outputs from all executors are collected. This method allows
|
||||
filtering to only include outputs from specified executors.
|
||||
|
||||
Args:
|
||||
executors: A list of executors or registered names of the executor factories
|
||||
whose outputs should be collected.
|
||||
|
||||
Returns:
|
||||
Self: The WorkflowBuilder instance for method chaining.
|
||||
"""
|
||||
self._output_executors = list(executors)
|
||||
return self
|
||||
|
||||
def _resolve_edge_registry(self) -> tuple[Executor, dict[str, Executor], list[EdgeGroup]]:
|
||||
"""Resolve deferred edge registrations into executors and edge groups.
|
||||
|
||||
@@ -1097,7 +950,9 @@ class WorkflowBuilder:
|
||||
as they are already part of the workflow builder's internal state.
|
||||
"""
|
||||
if not self._start_executor:
|
||||
raise ValueError("Starting executor must be set using set_start_executor before building the workflow.")
|
||||
raise ValueError(
|
||||
"Starting executor must be set via the start_executor constructor parameter before building."
|
||||
)
|
||||
|
||||
start_executor: Executor | None = None
|
||||
if isinstance(self._start_executor, Executor):
|
||||
@@ -1200,9 +1055,8 @@ class WorkflowBuilder:
|
||||
|
||||
# Build and execute a workflow
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor="MyExecutor")
|
||||
.register_executor(lambda: MyExecutor(id="executor"), name="MyExecutor")
|
||||
.set_start_executor("MyExecutor")
|
||||
.build()
|
||||
)
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ async def test_agent_executor_checkpoint_stores_and_restores_state() -> None:
|
||||
executor = AgentExecutor(initial_agent, agent_thread=initial_thread)
|
||||
|
||||
# Build workflow with checkpointing enabled
|
||||
wf = SequentialBuilder().participants([executor]).with_checkpointing(storage).build()
|
||||
wf = SequentialBuilder(participants=[executor], checkpoint_storage=storage).build()
|
||||
|
||||
# Run the workflow with a user message
|
||||
first_run_output: AgentExecutorResponse | None = None
|
||||
@@ -124,7 +124,7 @@ async def test_agent_executor_checkpoint_stores_and_restores_state() -> None:
|
||||
assert restored_agent.call_count == 0
|
||||
|
||||
# Build new workflow with the restored executor
|
||||
wf_resume = SequentialBuilder().participants([restored_executor]).with_checkpointing(storage).build()
|
||||
wf_resume = SequentialBuilder(participants=[restored_executor], checkpoint_storage=storage).build()
|
||||
|
||||
# Resume from checkpoint
|
||||
resumed_output: AgentExecutorResponse | None = None
|
||||
|
||||
@@ -96,7 +96,7 @@ async def test_agent_executor_emits_tool_calls_in_streaming_mode() -> None:
|
||||
agent = _ToolCallingAgent(id="tool_agent", name="ToolAgent")
|
||||
agent_exec = AgentExecutor(agent, id="tool_exec")
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(agent_exec).build()
|
||||
workflow = WorkflowBuilder(start_executor=agent_exec).build()
|
||||
|
||||
# Act: run in streaming mode
|
||||
events: list[WorkflowEvent[AgentResponseUpdate]] = []
|
||||
@@ -249,11 +249,7 @@ async def test_agent_executor_tool_call_with_approval() -> None:
|
||||
)
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(agent)
|
||||
.add_edge(agent, test_executor)
|
||||
.with_output_from([test_executor])
|
||||
.build()
|
||||
WorkflowBuilder(start_executor=agent, output_executors=[test_executor]).add_edge(agent, test_executor).build()
|
||||
)
|
||||
|
||||
# Act
|
||||
@@ -286,7 +282,7 @@ async def test_agent_executor_tool_call_with_approval_streaming() -> None:
|
||||
tools=[mock_tool_requiring_approval],
|
||||
)
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(agent).add_edge(agent, test_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=agent).add_edge(agent, test_executor).build()
|
||||
|
||||
# Act
|
||||
request_info_events: list[WorkflowEvent] = []
|
||||
@@ -324,11 +320,7 @@ async def test_agent_executor_parallel_tool_call_with_approval() -> None:
|
||||
)
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(agent)
|
||||
.add_edge(agent, test_executor)
|
||||
.with_output_from([test_executor])
|
||||
.build()
|
||||
WorkflowBuilder(start_executor=agent, output_executors=[test_executor]).add_edge(agent, test_executor).build()
|
||||
)
|
||||
|
||||
# Act
|
||||
@@ -363,7 +355,7 @@ async def test_agent_executor_parallel_tool_call_with_approval_streaming() -> No
|
||||
tools=[mock_tool_requiring_approval],
|
||||
)
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(agent).add_edge(agent, test_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=agent).add_edge(agent, test_executor).build()
|
||||
|
||||
# Act
|
||||
request_info_events: list[WorkflowEvent] = []
|
||||
|
||||
@@ -30,8 +30,9 @@ def build_workflow(storage: InMemoryCheckpointStorage, finish_id: str = "finish"
|
||||
start = StartExecutor(id="start")
|
||||
finish = FinishExecutor(id=finish_id)
|
||||
|
||||
builder = WorkflowBuilder(max_iterations=3).set_start_executor(start).add_edge(start, finish)
|
||||
builder = builder.with_checkpointing(checkpoint_storage=storage)
|
||||
builder = WorkflowBuilder(max_iterations=3, start_executor=start, checkpoint_storage=storage).add_edge(
|
||||
start, finish
|
||||
)
|
||||
return builder.build()
|
||||
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ async def test_executor_invoked_event_contains_input_data():
|
||||
upper = UpperCaseExecutor(id="upper")
|
||||
collector = CollectorExecutor(id="collector")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(upper, collector).set_start_executor(upper).build()
|
||||
workflow = WorkflowBuilder(start_executor=upper).add_edge(upper, collector).build()
|
||||
|
||||
events = await workflow.run("hello world")
|
||||
invoked_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_invoked"]
|
||||
@@ -190,7 +190,7 @@ async def test_executor_completed_event_contains_sent_messages():
|
||||
sender = MultiSenderExecutor(id="sender")
|
||||
collector = CollectorExecutor(id="collector")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(sender, collector).set_start_executor(sender).build()
|
||||
workflow = WorkflowBuilder(start_executor=sender).add_edge(sender, collector).build()
|
||||
|
||||
events = await workflow.run("hello")
|
||||
completed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_completed"]
|
||||
@@ -217,7 +217,7 @@ async def test_executor_completed_event_includes_yielded_outputs():
|
||||
await ctx.yield_output(text.upper())
|
||||
|
||||
executor = YieldOnlyExecutor(id="yielder")
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
|
||||
events = await workflow.run("test")
|
||||
completed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_completed"]
|
||||
@@ -260,7 +260,7 @@ async def test_executor_events_with_complex_message_types():
|
||||
processor = ProcessorExecutor(id="processor")
|
||||
collector = CollectorExecutor(id="collector")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(processor, collector).set_start_executor(processor).build()
|
||||
workflow = WorkflowBuilder(start_executor=processor).add_edge(processor, collector).build()
|
||||
|
||||
input_request = Request(query="hello", limit=3)
|
||||
events = await workflow.run(input_request)
|
||||
@@ -539,7 +539,7 @@ async def test_executor_invoked_event_data_not_mutated_by_handler():
|
||||
# Verify mutation happened
|
||||
assert len(messages) == original_len + 1
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(mutator).build()
|
||||
workflow = WorkflowBuilder(start_executor=mutator).build()
|
||||
|
||||
# Run with a single user message
|
||||
input_messages = [ChatMessage(role="user", text="hello")]
|
||||
|
||||
@@ -76,13 +76,7 @@ async def test_agent_executor_populates_full_conversation_non_streaming() -> Non
|
||||
agent_exec = AgentExecutor(agent, id="agent1-exec")
|
||||
capturer = _CaptureFullConversation(id="capture")
|
||||
|
||||
wf = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(agent_exec)
|
||||
.add_edge(agent_exec, capturer)
|
||||
.with_output_from([capturer])
|
||||
.build()
|
||||
)
|
||||
wf = WorkflowBuilder(start_executor=agent_exec, output_executors=[capturer]).add_edge(agent_exec, capturer).build()
|
||||
|
||||
# Act: use run() to test non-streaming mode
|
||||
result = await wf.run("hello world")
|
||||
@@ -144,7 +138,7 @@ async def test_sequential_adapter_uses_full_conversation() -> None:
|
||||
a1 = _CaptureAgent(id="agent1", name="A1", reply_text="A1 reply")
|
||||
a2 = _CaptureAgent(id="agent2", name="A2", reply_text="A2 reply")
|
||||
|
||||
wf = SequentialBuilder().participants([a1, a2]).build()
|
||||
wf = SequentialBuilder(participants=[a1, a2]).build()
|
||||
|
||||
# Act
|
||||
async for ev in wf.run("hello seq", stream=True):
|
||||
|
||||
@@ -236,7 +236,7 @@ class TestFunctionExecutor:
|
||||
assert reverse_spec["output_types"] == [Any] # First parameter is Any
|
||||
assert reverse_spec["workflow_output_types"] == [str] # Second parameter is str
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(to_upper, reverse_text).set_start_executor(to_upper).build()
|
||||
workflow = WorkflowBuilder(start_executor=to_upper).add_edge(to_upper, reverse_text).build()
|
||||
|
||||
# Run workflow
|
||||
events = await workflow.run("hello world")
|
||||
@@ -345,7 +345,7 @@ class TestFunctionExecutor:
|
||||
|
||||
# Since single-parameter functions can't send messages,
|
||||
# they're typically used as terminal nodes or for side effects
|
||||
WorkflowBuilder().set_start_executor(double_value).build()
|
||||
WorkflowBuilder(start_executor=double_value).build()
|
||||
|
||||
# For testing purposes, we can check that the handler is registered correctly
|
||||
assert double_value.can_handle(Message(data=5, source_id="mock"))
|
||||
|
||||
@@ -178,7 +178,7 @@ class TestRequestInfoAndResponse:
|
||||
async def test_approval_workflow(self):
|
||||
"""Test end-to-end workflow with approval request."""
|
||||
executor = ApprovalRequiredExecutor(id="approval_executor")
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
|
||||
# First run the workflow until it emits a request
|
||||
request_info_event: WorkflowEvent | None = None
|
||||
@@ -203,7 +203,7 @@ class TestRequestInfoAndResponse:
|
||||
async def test_calculation_workflow(self):
|
||||
"""Test end-to-end workflow with calculation request."""
|
||||
executor = CalculationExecutor(id="calc_executor")
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
|
||||
# First run the workflow until it emits a calculation request
|
||||
request_info_event: WorkflowEvent | None = None
|
||||
@@ -230,7 +230,7 @@ class TestRequestInfoAndResponse:
|
||||
async def test_multiple_requests_workflow(self):
|
||||
"""Test workflow with multiple concurrent requests."""
|
||||
executor = MultiRequestExecutor(id="multi_executor")
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
|
||||
# Collect all request events by running the full stream
|
||||
request_events: list[WorkflowEvent] = []
|
||||
@@ -264,7 +264,7 @@ class TestRequestInfoAndResponse:
|
||||
async def test_denied_approval_workflow(self):
|
||||
"""Test workflow when approval is denied."""
|
||||
executor = ApprovalRequiredExecutor(id="approval_executor")
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
|
||||
# First run the workflow until it emits a request
|
||||
request_info_event: WorkflowEvent | None = None
|
||||
@@ -287,7 +287,7 @@ class TestRequestInfoAndResponse:
|
||||
async def test_workflow_state_with_pending_requests(self):
|
||||
"""Test workflow state when waiting for responses."""
|
||||
executor = ApprovalRequiredExecutor(id="approval_executor")
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
|
||||
# Run workflow until idle with pending requests
|
||||
request_info_event: WorkflowEvent | None = None
|
||||
@@ -312,7 +312,7 @@ class TestRequestInfoAndResponse:
|
||||
async def test_invalid_calculation_input(self):
|
||||
"""Test workflow handling of invalid calculation input."""
|
||||
executor = CalculationExecutor(id="calc_executor")
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
|
||||
# Send invalid input (no numbers)
|
||||
completed = False
|
||||
@@ -334,7 +334,7 @@ class TestRequestInfoAndResponse:
|
||||
|
||||
# Create workflow with checkpointing enabled
|
||||
executor = ApprovalRequiredExecutor(id="approval_executor")
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).with_checkpointing(storage).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor, checkpoint_storage=storage).build()
|
||||
|
||||
# Step 1: Run workflow to completion to ensure checkpoints are created
|
||||
request_info_event: WorkflowEvent | None = None
|
||||
@@ -372,7 +372,7 @@ class TestRequestInfoAndResponse:
|
||||
|
||||
# Step 4: Create a fresh workflow and restore from checkpoint
|
||||
new_executor = ApprovalRequiredExecutor(id="approval_executor")
|
||||
restored_workflow = WorkflowBuilder().set_start_executor(new_executor).with_checkpointing(storage).build()
|
||||
restored_workflow = WorkflowBuilder(start_executor=new_executor, checkpoint_storage=storage).build()
|
||||
|
||||
# Step 5: Resume from checkpoint and verify the request can be continued
|
||||
completed = False
|
||||
|
||||
@@ -413,16 +413,14 @@ class TestSerializationWorkflowClasses:
|
||||
"""
|
||||
# Create innermost workflow
|
||||
inner_executor = SampleExecutor(id="inner-exec")
|
||||
inner_workflow = WorkflowBuilder().set_start_executor(inner_executor).set_max_iterations(10).build()
|
||||
inner_workflow = WorkflowBuilder(max_iterations=10, start_executor=inner_executor).build()
|
||||
|
||||
# Create middle workflow with WorkflowExecutor
|
||||
inner_workflow_executor = WorkflowExecutor(workflow=inner_workflow, id="inner-workflow-exec")
|
||||
middle_executor = SampleExecutor(id="middle-exec")
|
||||
middle_workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(middle_executor)
|
||||
WorkflowBuilder(max_iterations=20, start_executor=middle_executor)
|
||||
.add_edge(middle_executor, inner_workflow_executor)
|
||||
.set_max_iterations(20)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -430,10 +428,8 @@ class TestSerializationWorkflowClasses:
|
||||
middle_workflow_executor = WorkflowExecutor(workflow=middle_workflow, id="middle-workflow-exec")
|
||||
outer_executor = SampleExecutor(id="outer-exec")
|
||||
outer_workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(outer_executor)
|
||||
WorkflowBuilder(max_iterations=30, start_executor=outer_executor)
|
||||
.add_edge(outer_executor, middle_workflow_executor)
|
||||
.set_max_iterations(30)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -543,7 +539,7 @@ class TestSerializationWorkflowClasses:
|
||||
executor1 = SampleExecutor(id="executor1")
|
||||
executor2 = SampleExecutor(id="executor2")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
|
||||
# Test model_dump
|
||||
data = workflow.to_dict()
|
||||
@@ -616,7 +612,7 @@ class TestSerializationWorkflowClasses:
|
||||
executor1 = SampleExecutor(id="executor1")
|
||||
executor2 = SampleExecutor(id="executor2")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
|
||||
# Test model_dump - should not include private runtime objects
|
||||
data = workflow.to_dict()
|
||||
@@ -629,11 +625,11 @@ class TestSerializationWorkflowClasses:
|
||||
def test_workflow_name_description_serialization(self) -> None:
|
||||
"""Test that workflow name and description are serialized correctly."""
|
||||
# Test 1: With name and description
|
||||
workflow1 = (
|
||||
WorkflowBuilder(name="Test Pipeline", description="Test workflow description")
|
||||
.set_start_executor(SampleExecutor(id="e1"))
|
||||
.build()
|
||||
)
|
||||
workflow1 = WorkflowBuilder(
|
||||
name="Test Pipeline",
|
||||
description="Test workflow description",
|
||||
start_executor=SampleExecutor(id="e1"),
|
||||
).build()
|
||||
|
||||
assert workflow1.name == "Test Pipeline"
|
||||
assert workflow1.description == "Test workflow description"
|
||||
@@ -649,7 +645,7 @@ class TestSerializationWorkflowClasses:
|
||||
assert parsed1["description"] == "Test workflow description"
|
||||
|
||||
# Test 2: Without name and description (defaults)
|
||||
workflow2 = WorkflowBuilder().set_start_executor(SampleExecutor(id="e2")).build()
|
||||
workflow2 = WorkflowBuilder(start_executor=SampleExecutor(id="e2")).build()
|
||||
|
||||
assert workflow2.name is None
|
||||
assert workflow2.description is None
|
||||
@@ -659,7 +655,7 @@ class TestSerializationWorkflowClasses:
|
||||
assert "description" not in data2
|
||||
|
||||
# Test 3: With only name (no description)
|
||||
workflow3 = WorkflowBuilder(name="Named Only").set_start_executor(SampleExecutor(id="e3")).build()
|
||||
workflow3 = WorkflowBuilder(name="Named Only", start_executor=SampleExecutor(id="e3")).build()
|
||||
|
||||
assert workflow3.name == "Named Only"
|
||||
assert workflow3.description is None
|
||||
@@ -706,8 +702,7 @@ def test_comprehensive_edge_groups_workflow_serialization() -> None:
|
||||
|
||||
# Build workflow with all three edge group types
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(router)
|
||||
WorkflowBuilder(start_executor=router)
|
||||
# 1. SwitchCaseEdgeGroup: Conditional routing
|
||||
.add_switch_case_edge_group(
|
||||
router,
|
||||
|
||||
@@ -167,8 +167,7 @@ def create_email_validation_workflow() -> Workflow:
|
||||
email_domain_validator = EmailDomainValidator()
|
||||
|
||||
return (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(email_format_validator)
|
||||
WorkflowBuilder(start_executor=email_format_validator)
|
||||
.add_edge(email_format_validator, email_domain_validator)
|
||||
.build()
|
||||
)
|
||||
@@ -184,8 +183,7 @@ async def test_basic_sub_workflow() -> None:
|
||||
workflow_executor = WorkflowExecutor(validation_workflow, "email_validation_workflow")
|
||||
|
||||
main_workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(parent)
|
||||
WorkflowBuilder(start_executor=parent)
|
||||
.add_edge(parent, workflow_executor)
|
||||
.add_edge(workflow_executor, parent)
|
||||
.build()
|
||||
@@ -223,8 +221,7 @@ async def test_sub_workflow_with_interception():
|
||||
workflow_executor = WorkflowExecutor(validation_workflow, "email_workflow")
|
||||
|
||||
main_workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(parent)
|
||||
WorkflowBuilder(start_executor=parent)
|
||||
.add_edge(parent, workflow_executor)
|
||||
.add_edge(workflow_executor, parent)
|
||||
.build()
|
||||
@@ -340,8 +337,7 @@ async def test_workflow_scoped_interception() -> None:
|
||||
executor_b = WorkflowExecutor(workflow_b, "workflow_b")
|
||||
|
||||
main_workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(parent)
|
||||
WorkflowBuilder(start_executor=parent)
|
||||
.add_edge(parent, executor_a)
|
||||
.add_edge(parent, executor_b)
|
||||
.add_edge(executor_a, parent)
|
||||
@@ -422,8 +418,7 @@ async def test_concurrent_sub_workflow_execution() -> None:
|
||||
workflow_executor = WorkflowExecutor(validation_workflow, "email_workflow")
|
||||
|
||||
main_workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(processor)
|
||||
WorkflowBuilder(start_executor=processor)
|
||||
.add_edge(processor, workflow_executor)
|
||||
.add_edge(workflow_executor, processor)
|
||||
.build()
|
||||
@@ -564,16 +559,14 @@ class CheckpointTestCoordinator(Executor):
|
||||
def _build_checkpoint_test_workflow(storage: InMemoryCheckpointStorage) -> Workflow:
|
||||
"""Build the main workflow with checkpointing for testing."""
|
||||
two_step_executor = TwoStepSubWorkflowExecutor()
|
||||
sub_workflow = WorkflowBuilder().set_start_executor(two_step_executor).build()
|
||||
sub_workflow = WorkflowBuilder(start_executor=two_step_executor).build()
|
||||
sub_workflow_executor = WorkflowExecutor(sub_workflow, id="sub_workflow_executor")
|
||||
|
||||
coordinator = CheckpointTestCoordinator()
|
||||
return (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(coordinator)
|
||||
WorkflowBuilder(start_executor=coordinator, checkpoint_storage=storage)
|
||||
.add_edge(coordinator, sub_workflow_executor)
|
||||
.add_edge(sub_workflow_executor, coordinator)
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
|
||||
|
||||
@@ -69,9 +69,8 @@ def test_valid_workflow_passes_validation():
|
||||
|
||||
# Create a valid workflow
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=executor1)
|
||||
.add_edge(executor1, executor2)
|
||||
.set_start_executor(executor1)
|
||||
.build() # This should not raise any exceptions
|
||||
)
|
||||
|
||||
@@ -83,7 +82,7 @@ def test_duplicate_executor_ids_fail_validation():
|
||||
executor2 = IntExecutor(id="dup")
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
(WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build())
|
||||
(WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build())
|
||||
|
||||
assert str(exc_info.value) == "Duplicate executor ID 'dup' detected in workflow."
|
||||
|
||||
@@ -93,9 +92,7 @@ def test_edge_duplication_validation_fails():
|
||||
executor2 = StringExecutor(id="executor2")
|
||||
|
||||
with pytest.raises(EdgeDuplicationError) as exc_info:
|
||||
WorkflowBuilder().add_edge(executor1, executor2).add_edge(executor1, executor2).set_start_executor(
|
||||
executor1
|
||||
).build()
|
||||
WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).add_edge(executor1, executor2).build()
|
||||
|
||||
assert "executor1->executor2" in str(exc_info.value)
|
||||
assert exc_info.value.validation_type == ValidationTypeEnum.EDGE_DUPLICATION
|
||||
@@ -106,7 +103,7 @@ def test_type_compatibility_validation_fails():
|
||||
int_executor = IntExecutor(id="int_executor")
|
||||
|
||||
with pytest.raises(TypeCompatibilityError) as exc_info:
|
||||
WorkflowBuilder().add_edge(string_executor, int_executor).set_start_executor(string_executor).build()
|
||||
WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, int_executor).build()
|
||||
|
||||
error = exc_info.value
|
||||
assert error.source_executor_id == "string_executor"
|
||||
@@ -119,7 +116,7 @@ def test_type_compatibility_with_any_type_passes():
|
||||
any_executor = AnyExecutor(id="any_executor")
|
||||
|
||||
# This should not raise an exception
|
||||
workflow = WorkflowBuilder().add_edge(string_executor, any_executor).set_start_executor(string_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, any_executor).build()
|
||||
|
||||
assert workflow is not None
|
||||
|
||||
@@ -129,9 +126,7 @@ def test_type_compatibility_with_no_output_types():
|
||||
string_executor = StringExecutor(id="string_executor")
|
||||
|
||||
# This should pass validation since no output types are specified
|
||||
workflow = (
|
||||
WorkflowBuilder().add_edge(no_output_executor, string_executor).set_start_executor(no_output_executor).build()
|
||||
)
|
||||
workflow = WorkflowBuilder(start_executor=no_output_executor).add_edge(no_output_executor, string_executor).build()
|
||||
|
||||
assert workflow is not None
|
||||
|
||||
@@ -141,9 +136,7 @@ def test_multi_type_executor_compatibility():
|
||||
multi_type_executor = MultiTypeExecutor(id="multi_type")
|
||||
|
||||
# String executor outputs strings, multi-type can handle strings
|
||||
workflow = (
|
||||
WorkflowBuilder().add_edge(string_executor, multi_type_executor).set_start_executor(string_executor).build()
|
||||
)
|
||||
workflow = WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, multi_type_executor).build()
|
||||
|
||||
assert workflow is not None
|
||||
|
||||
@@ -154,9 +147,7 @@ def test_graph_connectivity_unreachable_executors():
|
||||
executor3 = StringExecutor(id="executor3") # This will be unreachable
|
||||
|
||||
with pytest.raises(GraphConnectivityError) as exc_info:
|
||||
WorkflowBuilder().add_edge(executor1, executor2).add_edge(executor3, executor2).set_start_executor(
|
||||
executor1
|
||||
).build()
|
||||
WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).add_edge(executor3, executor2).build()
|
||||
|
||||
assert "unreachable" in str(exc_info.value).lower()
|
||||
assert "executor3" in str(exc_info.value)
|
||||
@@ -189,19 +180,14 @@ def test_disconnected_start_executor_not_in_graph():
|
||||
executor3 = StringExecutor(id="executor3") # Not in graph
|
||||
|
||||
with pytest.raises(GraphConnectivityError) as exc_info:
|
||||
WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor3).build()
|
||||
WorkflowBuilder(start_executor=executor3).add_edge(executor1, executor2).build()
|
||||
|
||||
assert "The following executors are unreachable from the start executor 'executor3'" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_missing_start_executor():
|
||||
executor1 = StringExecutor(id="executor1")
|
||||
executor2 = StringExecutor(id="executor2")
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
WorkflowBuilder().add_edge(executor1, executor2).build()
|
||||
|
||||
assert "Starting executor must be set" in str(exc_info.value)
|
||||
with pytest.raises(TypeError):
|
||||
WorkflowBuilder() # type: ignore[call-arg]
|
||||
|
||||
|
||||
def test_workflow_validation_error_base_class():
|
||||
@@ -219,12 +205,11 @@ def test_complex_workflow_validation():
|
||||
executor4 = AnyExecutor(id="executor4")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=executor1)
|
||||
.add_edge(executor1, executor2) # str -> MultiType (compatible)
|
||||
.add_edge(executor2, executor3) # MultiType -> str (compatible)
|
||||
.add_edge(executor2, executor4) # MultiType -> Any (compatible)
|
||||
.add_edge(executor3, executor4) # str -> Any (compatible)
|
||||
.set_start_executor(executor1)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -246,7 +231,7 @@ def test_type_compatibility_inheritance():
|
||||
derived_executor = DerivedExecutor(id="derived")
|
||||
|
||||
# This should pass since both handle str
|
||||
workflow = WorkflowBuilder().add_edge(base_executor, derived_executor).set_start_executor(base_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=base_executor).add_edge(base_executor, derived_executor).build()
|
||||
|
||||
assert workflow is not None
|
||||
|
||||
@@ -271,7 +256,7 @@ def test_fan_out_validation():
|
||||
target1 = StringExecutor(id="target1")
|
||||
target2 = AnyExecutor(id="target2")
|
||||
|
||||
workflow = WorkflowBuilder().add_fan_out_edges(source, [target1, target2]).set_start_executor(source).build()
|
||||
workflow = WorkflowBuilder(start_executor=source).add_fan_out_edges(source, [target1, target2]).build()
|
||||
|
||||
assert workflow is not None
|
||||
|
||||
@@ -284,11 +269,10 @@ def test_fan_in_validation():
|
||||
|
||||
# Create a proper fan-in by having a start executor that connects to both sources
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=start_executor)
|
||||
.add_edge(start_executor, source1) # Start connects to source1
|
||||
.add_edge(start_executor, source2) # Start connects to source2
|
||||
.add_fan_in_edges([source1, source2], target) # Both sources fan-in to target
|
||||
.set_start_executor(start_executor)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -300,7 +284,7 @@ def test_chain_validation():
|
||||
executor2 = StringExecutor(id="executor2")
|
||||
executor3 = AnyExecutor(id="executor3")
|
||||
|
||||
workflow = WorkflowBuilder().add_chain([executor1, executor2, executor3]).set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_chain([executor1, executor2, executor3]).build()
|
||||
|
||||
assert workflow is not None
|
||||
|
||||
@@ -313,9 +297,7 @@ def test_logging_for_missing_output_types(caplog: Any) -> None:
|
||||
string_executor = StringExecutor(id="string_executor")
|
||||
|
||||
# This should trigger a warning log
|
||||
workflow = (
|
||||
WorkflowBuilder().add_edge(no_output_executor, string_executor).set_start_executor(no_output_executor).build()
|
||||
)
|
||||
workflow = WorkflowBuilder(start_executor=no_output_executor).add_edge(no_output_executor, string_executor).build()
|
||||
|
||||
assert workflow is not None
|
||||
assert "has no output type annotations" in caplog.text
|
||||
@@ -338,9 +320,7 @@ def test_logging_for_missing_input_types(caplog: Any) -> None:
|
||||
no_input_executor = NoInputTypesExecutor(id="no_input")
|
||||
|
||||
# This should pass since NoInputTypesExecutor has no proper input types
|
||||
workflow = (
|
||||
WorkflowBuilder().add_edge(string_executor, no_input_executor).set_start_executor(string_executor).build()
|
||||
)
|
||||
workflow = WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, no_input_executor).build()
|
||||
|
||||
assert workflow is not None
|
||||
|
||||
@@ -351,7 +331,7 @@ def test_self_loop_detection_warning(caplog: Any) -> None:
|
||||
executor = StringExecutor(id="self_loop_executor")
|
||||
|
||||
# Create a self-loop
|
||||
workflow = WorkflowBuilder().add_edge(executor, executor).set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).add_edge(executor, executor).build()
|
||||
|
||||
assert workflow is not None
|
||||
assert "Self-loop detected" in caplog.text
|
||||
@@ -365,7 +345,7 @@ def test_handler_validation_basic(caplog: Any) -> None:
|
||||
start_executor = StringExecutor(id="start")
|
||||
target_executor = StringExecutor(id="target")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(start_executor, target_executor).set_start_executor(start_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=start_executor).add_edge(start_executor, target_executor).build()
|
||||
|
||||
assert workflow is not None
|
||||
# Just ensure the validation runs without errors
|
||||
@@ -377,7 +357,7 @@ def test_dead_end_detection(caplog: Any) -> None:
|
||||
executor1 = StringExecutor(id="executor1")
|
||||
executor2 = StringExecutor(id="executor2") # This will be a dead end
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
|
||||
assert workflow is not None
|
||||
assert "Dead-end executors detected" in caplog.text
|
||||
@@ -391,7 +371,7 @@ def test_successful_type_compatibility_logging(caplog: Any) -> None:
|
||||
executor1 = StringExecutor(id="executor1")
|
||||
executor2 = StringExecutor(id="executor2")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
|
||||
assert workflow is not None
|
||||
assert "Type compatibility validated for edge" in caplog.text
|
||||
@@ -406,11 +386,7 @@ def test_multiple_dead_ends_detection(caplog: Any) -> None:
|
||||
executor3 = StringExecutor(id="executor3") # Dead end
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.add_edge(executor1, executor2)
|
||||
.add_edge(executor1, executor3)
|
||||
.set_start_executor(executor1)
|
||||
.build()
|
||||
WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).add_edge(executor1, executor3).build()
|
||||
)
|
||||
|
||||
assert workflow is not None
|
||||
@@ -426,7 +402,7 @@ def test_single_executor_workflow(caplog: Any) -> None:
|
||||
executor2 = StringExecutor(id="executor2")
|
||||
|
||||
# Create a simple two-executor workflow to avoid graph validation issues
|
||||
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
|
||||
assert workflow is not None
|
||||
# Should detect executor2 as dead end
|
||||
@@ -438,7 +414,7 @@ def test_enhanced_type_compatibility_error_details():
|
||||
int_executor = IntExecutor(id="int_executor")
|
||||
|
||||
with pytest.raises(TypeCompatibilityError) as exc_info:
|
||||
WorkflowBuilder().add_edge(string_executor, int_executor).set_start_executor(string_executor).build()
|
||||
WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, int_executor).build()
|
||||
|
||||
error = exc_info.value
|
||||
# Verify enhanced error contains detailed type information
|
||||
@@ -463,7 +439,7 @@ def test_union_type_compatibility_validation() -> None:
|
||||
union_input = UnionInputExecutor(id="union_input")
|
||||
|
||||
# This should pass validation due to type compatibility (str)
|
||||
workflow = WorkflowBuilder().add_edge(union_output, union_input).set_start_executor(union_output).build()
|
||||
workflow = WorkflowBuilder(start_executor=union_output).add_edge(union_output, union_input).build()
|
||||
|
||||
assert workflow is not None
|
||||
|
||||
@@ -483,7 +459,7 @@ def test_generic_type_compatibility() -> None:
|
||||
list_input = ListInputExecutor(id="list_input")
|
||||
|
||||
# This should pass validation for generic type compatibility
|
||||
workflow = WorkflowBuilder().add_edge(list_output, list_input).set_start_executor(list_output).build()
|
||||
workflow = WorkflowBuilder(start_executor=list_output).add_edge(list_output, list_input).build()
|
||||
|
||||
assert workflow is not None
|
||||
|
||||
@@ -539,7 +515,7 @@ def test_handler_ctx_none_is_allowed() -> None:
|
||||
none_exec = NoneExecutor(id="n")
|
||||
|
||||
# Should build successfully
|
||||
wf = WorkflowBuilder().add_edge(start, none_exec).set_start_executor(start).build()
|
||||
wf = WorkflowBuilder(start_executor=start).add_edge(start, none_exec).build()
|
||||
assert wf is not None
|
||||
|
||||
|
||||
@@ -555,7 +531,7 @@ def test_handler_ctx_any_is_allowed_but_skips_type_checks(caplog: Any) -> None:
|
||||
any_out = AnyOutExecutor(id="a")
|
||||
|
||||
# Builds; later edges from this executor will skip type compatibility when outputs are unspecified
|
||||
wf = WorkflowBuilder().add_edge(start, any_out).set_start_executor(start).build()
|
||||
wf = WorkflowBuilder(start_executor=start).add_edge(start, any_out).build()
|
||||
assert wf is not None
|
||||
|
||||
|
||||
@@ -575,11 +551,7 @@ def test_output_validation_with_valid_output_executors():
|
||||
|
||||
# Build workflow with valid output executors
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.add_edge(executor1, executor2)
|
||||
.set_start_executor(executor1)
|
||||
.with_output_from([executor2])
|
||||
.build()
|
||||
WorkflowBuilder(start_executor=executor1, output_executors=[executor2]).add_edge(executor1, executor2).build()
|
||||
)
|
||||
|
||||
assert workflow is not None
|
||||
@@ -593,11 +565,9 @@ def test_output_validation_with_multiple_valid_output_executors():
|
||||
executor3 = OutputExecutor(id="executor3")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=executor1, output_executors=[executor1, executor3])
|
||||
.add_edge(executor1, executor2)
|
||||
.add_edge(executor2, executor3)
|
||||
.set_start_executor(executor1)
|
||||
.with_output_from([executor1, executor3])
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -628,10 +598,8 @@ def test_output_validation_fails_for_executor_without_output_types():
|
||||
|
||||
with pytest.raises(WorkflowValidationError) as exc_info:
|
||||
(
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=executor1, output_executors=[no_output_executor])
|
||||
.add_edge(executor1, no_output_executor)
|
||||
.set_start_executor(executor1)
|
||||
.with_output_from([no_output_executor])
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -645,9 +613,7 @@ def test_output_validation_empty_list_passes():
|
||||
executor1 = OutputExecutor(id="executor1")
|
||||
executor2 = OutputExecutor(id="executor2")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).with_output_from([]).build()
|
||||
)
|
||||
workflow = WorkflowBuilder(start_executor=executor1, output_executors=[]).add_edge(executor1, executor2).build()
|
||||
|
||||
assert workflow is not None
|
||||
# All executors are outputs
|
||||
|
||||
@@ -31,7 +31,7 @@ def basic_sub_workflow():
|
||||
sub_exec1 = MockExecutor(id="sub_exec1")
|
||||
sub_exec2 = MockExecutor(id="sub_exec2")
|
||||
|
||||
sub_workflow = WorkflowBuilder().add_edge(sub_exec1, sub_exec2).set_start_executor(sub_exec1).build()
|
||||
sub_workflow = WorkflowBuilder(start_executor=sub_exec1).add_edge(sub_exec1, sub_exec2).build()
|
||||
|
||||
# Create a workflow executor that wraps the sub-workflow
|
||||
workflow_executor = WorkflowExecutor(sub_workflow, id="workflow_executor_1")
|
||||
@@ -41,10 +41,9 @@ def basic_sub_workflow():
|
||||
final_exec = MockExecutor(id="final_executor")
|
||||
|
||||
main_workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=main_exec)
|
||||
.add_edge(main_exec, workflow_executor)
|
||||
.add_edge(workflow_executor, final_exec)
|
||||
.set_start_executor(main_exec)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -65,7 +64,7 @@ def test_workflow_viz_to_digraph():
|
||||
executor1 = MockExecutor(id="executor1")
|
||||
executor2 = MockExecutor(id="executor2")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
|
||||
viz = WorkflowViz(workflow)
|
||||
dot_content = viz.to_digraph()
|
||||
@@ -84,7 +83,7 @@ def test_workflow_viz_export_dot():
|
||||
executor1 = MockExecutor(id="executor1")
|
||||
executor2 = MockExecutor(id="executor2")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
|
||||
viz = WorkflowViz(workflow)
|
||||
|
||||
@@ -104,7 +103,7 @@ def test_workflow_viz_export_dot_with_filename(tmp_path):
|
||||
executor1 = MockExecutor(id="executor1")
|
||||
executor2 = MockExecutor(id="executor2")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
|
||||
viz = WorkflowViz(workflow)
|
||||
|
||||
@@ -128,12 +127,11 @@ def test_workflow_viz_complex_workflow():
|
||||
executor4 = MockExecutor(id="end")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=executor1)
|
||||
.add_edge(executor1, executor2)
|
||||
.add_edge(executor1, executor3)
|
||||
.add_edge(executor2, executor4)
|
||||
.add_edge(executor3, executor4)
|
||||
.set_start_executor(executor1)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -162,7 +160,7 @@ def test_workflow_viz_export_svg():
|
||||
executor1 = MockExecutor(id="executor1")
|
||||
executor2 = MockExecutor(id="executor2")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
|
||||
viz = WorkflowViz(workflow)
|
||||
|
||||
@@ -178,7 +176,7 @@ def test_workflow_viz_unsupported_format():
|
||||
executor1 = MockExecutor(id="executor1")
|
||||
executor2 = MockExecutor(id="executor2")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
|
||||
viz = WorkflowViz(workflow)
|
||||
|
||||
@@ -196,7 +194,7 @@ def test_workflow_viz_graphviz_binary_not_found():
|
||||
executor1 = MockExecutor(id="executor1")
|
||||
executor2 = MockExecutor(id="executor2")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
viz = WorkflowViz(workflow)
|
||||
|
||||
# Mock graphviz.Source.render to raise ExecutableNotFound
|
||||
@@ -224,13 +222,7 @@ def test_workflow_viz_conditional_edge():
|
||||
def only_if_foo(msg: str) -> bool: # pragma: no cover - simple predicate
|
||||
return msg == "foo"
|
||||
|
||||
wf = (
|
||||
WorkflowBuilder()
|
||||
.add_edge(start, mid, condition=only_if_foo)
|
||||
.add_edge(mid, end)
|
||||
.set_start_executor(start)
|
||||
.build()
|
||||
)
|
||||
wf = WorkflowBuilder(start_executor=start).add_edge(start, mid, condition=only_if_foo).add_edge(mid, end).build()
|
||||
|
||||
dot = WorkflowViz(wf).to_digraph()
|
||||
|
||||
@@ -249,13 +241,7 @@ def test_workflow_viz_fan_in_edge_group():
|
||||
t = ListStrTargetExecutor(id="t")
|
||||
|
||||
# Build a connected workflow: start fans out to s1 and s2, which then fan-in to t
|
||||
wf = (
|
||||
WorkflowBuilder()
|
||||
.add_fan_out_edges(start, [s1, s2])
|
||||
.add_fan_in_edges([s1, s2], t)
|
||||
.set_start_executor(start)
|
||||
.build()
|
||||
)
|
||||
wf = WorkflowBuilder(start_executor=start).add_fan_out_edges(start, [s1, s2]).add_fan_in_edges([s1, s2], t).build()
|
||||
|
||||
dot = WorkflowViz(wf).to_digraph()
|
||||
|
||||
@@ -287,7 +273,7 @@ def test_workflow_viz_to_mermaid_basic():
|
||||
executor1 = MockExecutor(id="executor1")
|
||||
executor2 = MockExecutor(id="executor2")
|
||||
|
||||
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
mermaid = WorkflowViz(workflow).to_mermaid()
|
||||
|
||||
# Start node and normal node
|
||||
@@ -305,7 +291,7 @@ def test_workflow_viz_mermaid_conditional_edge():
|
||||
def only_if_foo(msg: str) -> bool: # pragma: no cover - simple predicate
|
||||
return msg == "foo"
|
||||
|
||||
wf = WorkflowBuilder().add_edge(start, mid, condition=only_if_foo).set_start_executor(start).build()
|
||||
wf = WorkflowBuilder(start_executor=start).add_edge(start, mid, condition=only_if_foo).build()
|
||||
mermaid = WorkflowViz(wf).to_mermaid()
|
||||
|
||||
assert "start -. conditional .-> mid" in mermaid
|
||||
@@ -318,13 +304,7 @@ def test_workflow_viz_mermaid_fan_in_edge_group():
|
||||
s2 = MockExecutor(id="s2")
|
||||
t = ListStrTargetExecutor(id="t")
|
||||
|
||||
wf = (
|
||||
WorkflowBuilder()
|
||||
.add_fan_out_edges(start, [s1, s2])
|
||||
.add_fan_in_edges([s1, s2], t)
|
||||
.set_start_executor(start)
|
||||
.build()
|
||||
)
|
||||
wf = WorkflowBuilder(start_executor=start).add_fan_out_edges(start, [s1, s2]).add_fan_in_edges([s1, s2], t).build()
|
||||
|
||||
mermaid = WorkflowViz(wf).to_mermaid()
|
||||
lines = [line.strip() for line in mermaid.splitlines()]
|
||||
@@ -398,23 +378,19 @@ def test_workflow_viz_nested_sub_workflows():
|
||||
"""Test visualization of deeply nested sub-workflows."""
|
||||
# Create innermost sub-workflow
|
||||
inner_exec = MockExecutor(id="inner_exec")
|
||||
inner_workflow = WorkflowBuilder().set_start_executor(inner_exec).build()
|
||||
inner_workflow = WorkflowBuilder(start_executor=inner_exec).build()
|
||||
|
||||
# Create middle sub-workflow that contains the inner one
|
||||
inner_workflow_executor = WorkflowExecutor(inner_workflow, id="inner_wf_exec")
|
||||
middle_exec = MockExecutor(id="middle_exec")
|
||||
|
||||
middle_workflow = (
|
||||
WorkflowBuilder().add_edge(middle_exec, inner_workflow_executor).set_start_executor(middle_exec).build()
|
||||
)
|
||||
middle_workflow = WorkflowBuilder(start_executor=middle_exec).add_edge(middle_exec, inner_workflow_executor).build()
|
||||
|
||||
# Create outer workflow
|
||||
middle_workflow_executor = WorkflowExecutor(middle_workflow, id="middle_wf_exec")
|
||||
outer_exec = MockExecutor(id="outer_exec")
|
||||
|
||||
outer_workflow = (
|
||||
WorkflowBuilder().add_edge(outer_exec, middle_workflow_executor).set_start_executor(outer_exec).build()
|
||||
)
|
||||
outer_workflow = WorkflowBuilder(start_executor=outer_exec).add_edge(outer_exec, middle_workflow_executor).build()
|
||||
|
||||
viz = WorkflowViz(outer_workflow)
|
||||
dot_content = viz.to_digraph()
|
||||
|
||||
@@ -110,8 +110,7 @@ async def test_workflow_run_streaming() -> None:
|
||||
executor_b = IncrementExecutor(id="executor_b")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
WorkflowBuilder(start_executor=executor_a)
|
||||
.add_edge(executor_a, executor_b)
|
||||
.add_edge(executor_b, executor_a)
|
||||
.build()
|
||||
@@ -132,11 +131,9 @@ async def test_workflow_run_stream_not_completed():
|
||||
executor_b = IncrementExecutor(id="executor_b")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
WorkflowBuilder(max_iterations=5, start_executor=executor_a)
|
||||
.add_edge(executor_a, executor_b)
|
||||
.add_edge(executor_b, executor_a)
|
||||
.set_max_iterations(5)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -151,8 +148,7 @@ async def test_workflow_run():
|
||||
executor_b = IncrementExecutor(id="executor_b")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
WorkflowBuilder(start_executor=executor_a)
|
||||
.add_edge(executor_a, executor_b)
|
||||
.add_edge(executor_b, executor_a)
|
||||
.build()
|
||||
@@ -170,11 +166,9 @@ async def test_workflow_run_not_completed():
|
||||
executor_b = IncrementExecutor(id="executor_b")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
WorkflowBuilder(max_iterations=5, start_executor=executor_a)
|
||||
.add_edge(executor_a, executor_b)
|
||||
.add_edge(executor_b, executor_a)
|
||||
.set_max_iterations(5)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -189,7 +183,7 @@ async def test_fan_out():
|
||||
executor_c = IncrementExecutor(id="executor_c", limit=2) # This executor will not complete the workflow
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder().set_start_executor(executor_a).add_fan_out_edges(executor_a, [executor_b, executor_c]).build()
|
||||
WorkflowBuilder(start_executor=executor_a).add_fan_out_edges(executor_a, [executor_b, executor_c]).build()
|
||||
)
|
||||
|
||||
events = await workflow.run(NumberMessage(data=0))
|
||||
@@ -214,7 +208,7 @@ async def test_fan_out_multiple_completed_events():
|
||||
executor_c = IncrementExecutor(id="executor_c", limit=1)
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder().set_start_executor(executor_a).add_fan_out_edges(executor_a, [executor_b, executor_c]).build()
|
||||
WorkflowBuilder(start_executor=executor_a).add_fan_out_edges(executor_a, [executor_b, executor_c]).build()
|
||||
)
|
||||
|
||||
events = await workflow.run(NumberMessage(data=0))
|
||||
@@ -239,8 +233,7 @@ async def test_fan_in():
|
||||
aggregator = AggregatorExecutor(id="aggregator")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
WorkflowBuilder(start_executor=executor_a)
|
||||
.add_fan_out_edges(executor_a, [executor_b, executor_c])
|
||||
.add_fan_in_edges([executor_b, executor_c], aggregator)
|
||||
.build()
|
||||
@@ -276,10 +269,8 @@ async def test_workflow_with_checkpointing_enabled(simple_executor: Executor):
|
||||
|
||||
# Build workflow with checkpointing - should not raise any errors
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=simple_executor, checkpoint_storage=storage)
|
||||
.add_edge(simple_executor, simple_executor) # Self-loop to satisfy graph requirements
|
||||
.set_start_executor(simple_executor)
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -295,9 +286,8 @@ async def test_workflow_checkpointing_not_enabled_for_external_restore(
|
||||
"""Test that external checkpoint restoration fails when workflow doesn't support checkpointing."""
|
||||
# Build workflow WITHOUT checkpointing
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=simple_executor)
|
||||
.add_edge(simple_executor, simple_executor) # Self-loop to satisfy graph requirements
|
||||
.set_start_executor(simple_executor)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -315,9 +305,8 @@ async def test_workflow_run_stream_from_checkpoint_no_checkpointing_enabled(
|
||||
):
|
||||
# Build workflow WITHOUT checkpointing
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=simple_executor)
|
||||
.add_edge(simple_executor, simple_executor) # Self-loop to satisfy graph requirements
|
||||
.set_start_executor(simple_executor)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -340,10 +329,8 @@ async def test_workflow_run_stream_from_checkpoint_invalid_checkpoint(
|
||||
|
||||
# Build workflow with checkpointing
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=simple_executor, checkpoint_storage=storage)
|
||||
.add_edge(simple_executor, simple_executor) # Self-loop to satisfy graph requirements
|
||||
.set_start_executor(simple_executor)
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -376,7 +363,7 @@ async def test_workflow_run_stream_from_checkpoint_with_external_storage(
|
||||
|
||||
# Create a workflow WITHOUT checkpointing
|
||||
workflow_without_checkpointing = (
|
||||
WorkflowBuilder().add_edge(simple_executor, simple_executor).set_start_executor(simple_executor).build()
|
||||
WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, simple_executor).build()
|
||||
)
|
||||
|
||||
# Resume from checkpoint using external storage parameter
|
||||
@@ -411,10 +398,8 @@ async def test_workflow_run_from_checkpoint_non_streaming(simple_executor: Execu
|
||||
|
||||
# Build workflow with checkpointing
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=simple_executor, checkpoint_storage=storage)
|
||||
.add_edge(simple_executor, simple_executor)
|
||||
.set_start_executor(simple_executor)
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -452,10 +437,8 @@ async def test_workflow_run_stream_from_checkpoint_with_responses(
|
||||
|
||||
# Build workflow with checkpointing
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=simple_executor, checkpoint_storage=storage)
|
||||
.add_edge(simple_executor, simple_executor)
|
||||
.set_start_executor(simple_executor)
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -512,10 +495,8 @@ async def test_workflow_multiple_runs_no_state_collision():
|
||||
|
||||
# Build workflow with checkpointing
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=state_executor, checkpoint_storage=storage)
|
||||
.add_edge(state_executor, state_executor) # Self-loop to satisfy graph requirements
|
||||
.set_start_executor(state_executor)
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -552,9 +533,7 @@ async def test_workflow_checkpoint_runtime_only_configuration(
|
||||
storage = FileCheckpointStorage(temp_dir)
|
||||
|
||||
# Build workflow WITHOUT checkpointing at build time
|
||||
workflow = (
|
||||
WorkflowBuilder().add_edge(simple_executor, simple_executor).set_start_executor(simple_executor).build()
|
||||
)
|
||||
workflow = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, simple_executor).build()
|
||||
|
||||
# Run with runtime checkpoint storage - should create checkpoints
|
||||
test_message = Message(data="runtime checkpoint test", source_id="test", target_id=None)
|
||||
@@ -575,7 +554,7 @@ async def test_workflow_checkpoint_runtime_only_configuration(
|
||||
|
||||
# Create new workflow instance (still without build-time checkpointing)
|
||||
workflow_resume = (
|
||||
WorkflowBuilder().add_edge(simple_executor, simple_executor).set_start_executor(simple_executor).build()
|
||||
WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, simple_executor).build()
|
||||
)
|
||||
|
||||
# Resume from checkpoint using runtime checkpoint storage
|
||||
@@ -602,10 +581,8 @@ async def test_workflow_checkpoint_runtime_overrides_buildtime(
|
||||
|
||||
# Build workflow with build-time checkpointing
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor=simple_executor, checkpoint_storage=buildtime_storage)
|
||||
.add_edge(simple_executor, simple_executor)
|
||||
.set_start_executor(simple_executor)
|
||||
.with_checkpointing(buildtime_storage)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -643,8 +620,7 @@ async def test_comprehensive_edge_groups_workflow():
|
||||
# 3. FanOut: fanout_hub -> [parallel_1, parallel_2]
|
||||
# 4. FanIn: [parallel_1, parallel_2] -> aggregator
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(router)
|
||||
WorkflowBuilder(start_executor=router)
|
||||
# Switch-case routing based on message data
|
||||
.add_switch_case_edge_group(
|
||||
router,
|
||||
@@ -713,8 +689,7 @@ async def test_workflow_with_simple_cycle_and_exit_condition():
|
||||
|
||||
# Simple cycle: A -> B -> A, A exits when limit reached
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
WorkflowBuilder(start_executor=executor_a)
|
||||
.add_edge(executor_a, executor_b) # A -> B
|
||||
.add_edge(executor_b, executor_a) # B -> A (creates cycle)
|
||||
.build()
|
||||
@@ -746,7 +721,7 @@ async def test_workflow_concurrent_execution_prevention():
|
||||
"""Test that concurrent workflow executions are prevented."""
|
||||
# Create a simple workflow that takes some time to execute
|
||||
executor = IncrementExecutor(id="slow_executor", limit=3, increment=1)
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
|
||||
# Create a task that will run the workflow
|
||||
async def run_workflow():
|
||||
@@ -778,7 +753,7 @@ async def test_workflow_concurrent_execution_prevention_streaming():
|
||||
"""Test that concurrent workflow streaming executions are prevented."""
|
||||
# Create a simple workflow
|
||||
executor = IncrementExecutor(id="slow_executor", limit=3, increment=1)
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
|
||||
# Create an async generator that will consume the stream slowly
|
||||
async def consume_stream_slowly():
|
||||
@@ -814,7 +789,7 @@ async def test_workflow_concurrent_execution_prevention_mixed_methods():
|
||||
"""Test that concurrent executions are prevented across different execution methods."""
|
||||
# Create a simple workflow
|
||||
executor = IncrementExecutor(id="slow_executor", limit=3, increment=1)
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
|
||||
# Start a streaming execution
|
||||
async def consume_stream():
|
||||
@@ -884,7 +859,7 @@ async def test_agent_streaming_vs_non_streaming() -> None:
|
||||
agent = _StreamingTestAgent(id="test_agent", name="TestAgent", reply_text="Hello World")
|
||||
agent_exec = AgentExecutor(agent, id="agent_exec")
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(agent_exec).build()
|
||||
workflow = WorkflowBuilder(start_executor=agent_exec).build()
|
||||
|
||||
# Test non-streaming mode with run()
|
||||
result = await workflow.run("test message")
|
||||
@@ -934,7 +909,7 @@ async def test_agent_streaming_vs_non_streaming() -> None:
|
||||
|
||||
async def test_workflow_run_parameter_validation(simple_executor: Executor) -> None:
|
||||
"""Test that stream properly validate parameter combinations."""
|
||||
workflow = WorkflowBuilder().add_edge(simple_executor, simple_executor).set_start_executor(simple_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, simple_executor).build()
|
||||
|
||||
test_message = Message(data="test", source_id="test", target_id=None)
|
||||
|
||||
@@ -965,7 +940,7 @@ async def test_workflow_run_stream_parameter_validation(
|
||||
simple_executor: Executor,
|
||||
) -> None:
|
||||
"""Test stream=True specific parameter validation scenarios."""
|
||||
workflow = WorkflowBuilder().add_edge(simple_executor, simple_executor).set_start_executor(simple_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, simple_executor).build()
|
||||
|
||||
test_message = Message(data="test", source_id="test", target_id=None)
|
||||
|
||||
@@ -1014,7 +989,7 @@ async def test_output_executors_empty_yields_all_outputs() -> None:
|
||||
executor_b = OutputProducerExecutor(id="executor_b", output_value=20)
|
||||
|
||||
# Build workflow with a -> b
|
||||
workflow = WorkflowBuilder().set_start_executor(executor_a).add_edge(executor_a, executor_b).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor_a).add_edge(executor_a, executor_b).build()
|
||||
|
||||
result = await workflow.run(NumberMessage(data=0))
|
||||
outputs = result.get_outputs()
|
||||
@@ -1037,10 +1012,8 @@ async def test_output_executors_filters_outputs_non_streaming() -> None:
|
||||
|
||||
# Build workflow with a -> b
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_b])
|
||||
.add_edge(executor_a, executor_b)
|
||||
.with_output_from([executor_b])
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -1064,10 +1037,8 @@ async def test_output_executors_filters_outputs_streaming() -> None:
|
||||
|
||||
# Build workflow with a -> b
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_a])
|
||||
.add_edge(executor_a, executor_b)
|
||||
.with_output_from([executor_a])
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -1092,11 +1063,9 @@ async def test_output_executors_with_multiple_specified_executors() -> None:
|
||||
|
||||
# Build workflow with a -> b -> c
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_a, executor_c])
|
||||
.add_edge(executor_a, executor_b)
|
||||
.add_edge(executor_b, executor_c)
|
||||
.with_output_from([executor_a, executor_c])
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -1114,7 +1083,7 @@ async def test_output_executors_with_nonexistent_executor_id() -> None:
|
||||
"""Test that specifying a non-existent executor ID doesn't break the workflow."""
|
||||
executor_a = OutputProducerExecutor(id="executor_a", output_value=42)
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(executor_a).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor_a).build()
|
||||
|
||||
# Set output_executors to an ID that doesn't exist
|
||||
workflow._output_executors = ["nonexistent_executor"] # type: ignore
|
||||
@@ -1157,11 +1126,9 @@ async def test_output_executors_filtering_with_fan_in() -> None:
|
||||
|
||||
# Build fan-in workflow: start -> [a, b] -> aggregator
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_start)
|
||||
WorkflowBuilder(start_executor=executor_start, output_executors=[aggregator])
|
||||
.add_fan_out_edges(executor_start, [executor_a, executor_b])
|
||||
.add_fan_in_edges([executor_a, executor_b], aggregator)
|
||||
.with_output_from([aggregator])
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -1178,7 +1145,7 @@ async def test_output_executors_filtering_with_run_responses() -> None:
|
||||
"""Test output filtering works correctly with run(responses=...) method."""
|
||||
executor = MockExecutorRequestApproval(id="approval_executor")
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).with_output_from([executor]).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor, output_executors=[executor]).build()
|
||||
|
||||
# Run workflow which will request approval
|
||||
result = await workflow.run(NumberMessage(data=42))
|
||||
@@ -1201,7 +1168,7 @@ async def test_output_executors_filtering_with_run_responses_streaming() -> None
|
||||
"""Test output filtering works correctly with run(responses=..., stream=True) method."""
|
||||
executor = MockExecutorRequestApproval(id="approval_executor")
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
|
||||
# Run workflow which will request approval
|
||||
events_list: list[WorkflowEvent] = []
|
||||
|
||||
@@ -150,7 +150,7 @@ class TestWorkflowAgent:
|
||||
executor1 = SimpleExecutor(id="executor1", response_text="Step1", streaming=False)
|
||||
executor2 = SimpleExecutor(id="executor2", response_text="Step2", streaming=False)
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(executor1).add_edge(executor1, executor2).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
|
||||
agent = WorkflowAgent(workflow=workflow, name="Test Agent")
|
||||
|
||||
@@ -194,7 +194,7 @@ class TestWorkflowAgent:
|
||||
executor2 = SimpleExecutor(id="stream2", response_text="Streaming2")
|
||||
|
||||
# Create workflow with just one executor
|
||||
workflow = WorkflowBuilder().set_start_executor(executor1).add_edge(executor1, executor2).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
|
||||
agent = WorkflowAgent(workflow=workflow, name="Streaming Test Agent")
|
||||
|
||||
@@ -224,7 +224,7 @@ class TestWorkflowAgent:
|
||||
requesting_executor = RequestingExecutor(id="requester", streaming=False)
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder().set_start_executor(simple_executor).add_edge(simple_executor, requesting_executor).build()
|
||||
WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, requesting_executor).build()
|
||||
)
|
||||
|
||||
agent = WorkflowAgent(workflow=workflow, name="Request Test Agent")
|
||||
@@ -296,7 +296,7 @@ class TestWorkflowAgent:
|
||||
"""Test that Workflow.as_agent() creates a properly configured WorkflowAgent."""
|
||||
# Create a simple workflow
|
||||
executor = SimpleExecutor(id="executor1", response_text="Response")
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
|
||||
# Test as_agent with a name
|
||||
agent = workflow.as_agent(name="TestAgent")
|
||||
@@ -322,7 +322,7 @@ class TestWorkflowAgent:
|
||||
|
||||
# Create a simple workflow
|
||||
executor = _Executor(id="test")
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
|
||||
# Try to create an agent with unsupported input types
|
||||
with pytest.raises(ValueError, match="Workflow's start executor cannot handle list\\[ChatMessage\\]"):
|
||||
@@ -341,7 +341,7 @@ class TestWorkflowAgent:
|
||||
input_text = messages[0].text if messages else "no input"
|
||||
await ctx.yield_output(f"processed: {input_text}")
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(yielding_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=yielding_executor).build()
|
||||
|
||||
# Run directly - should return output event (type='output') in result
|
||||
direct_result = await workflow.run([ChatMessage(role="user", text="hello")])
|
||||
@@ -365,7 +365,7 @@ class TestWorkflowAgent:
|
||||
await ctx.yield_output("first output")
|
||||
await ctx.yield_output("second output")
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(yielding_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=yielding_executor).build()
|
||||
agent = workflow.as_agent("test-agent")
|
||||
|
||||
updates: list[AgentResponseUpdate] = []
|
||||
@@ -387,7 +387,7 @@ class TestWorkflowAgent:
|
||||
await ctx.yield_output(Content.from_data(data=b"binary data", media_type="application/octet-stream"))
|
||||
await ctx.yield_output(Content.from_uri(uri="https://example.com/image.png", media_type="image/png"))
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(content_yielding_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=content_yielding_executor).build()
|
||||
agent = workflow.as_agent("content-test-agent")
|
||||
|
||||
result = await agent.run("test")
|
||||
@@ -417,7 +417,7 @@ class TestWorkflowAgent:
|
||||
)
|
||||
await ctx.yield_output(msg)
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(chat_message_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=chat_message_executor).build()
|
||||
agent = workflow.as_agent("chat-msg-agent")
|
||||
|
||||
result = await agent.run("test")
|
||||
@@ -448,7 +448,7 @@ class TestWorkflowAgent:
|
||||
custom = CustomData(42)
|
||||
await ctx.yield_output(custom)
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(raw_yielding_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=raw_yielding_executor).build()
|
||||
agent = workflow.as_agent("raw-test-agent")
|
||||
|
||||
updates: list[AgentResponseUpdate] = []
|
||||
@@ -490,7 +490,7 @@ class TestWorkflowAgent:
|
||||
]
|
||||
await ctx.yield_output(msg_list)
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(list_yielding_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=list_yielding_executor).build()
|
||||
agent = workflow.as_agent("list-msg-agent")
|
||||
|
||||
# Verify streaming returns the update with all 4 contents before coalescing
|
||||
@@ -521,7 +521,7 @@ class TestWorkflowAgent:
|
||||
"""
|
||||
# Create an executor that captures all received messages
|
||||
capturing_executor = ConversationHistoryCapturingExecutor(id="capturing", streaming=False)
|
||||
workflow = WorkflowBuilder().set_start_executor(capturing_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=capturing_executor).build()
|
||||
agent = WorkflowAgent(workflow=workflow, name="Thread History Test Agent")
|
||||
|
||||
# Create a thread with existing conversation history
|
||||
@@ -551,7 +551,7 @@ class TestWorkflowAgent:
|
||||
"""
|
||||
# Create an executor that captures all received messages
|
||||
capturing_executor = ConversationHistoryCapturingExecutor(id="capturing_stream")
|
||||
workflow = WorkflowBuilder().set_start_executor(capturing_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=capturing_executor).build()
|
||||
agent = WorkflowAgent(workflow=workflow, name="Thread Stream Test Agent")
|
||||
|
||||
# Create a thread with existing conversation history
|
||||
@@ -579,7 +579,7 @@ class TestWorkflowAgent:
|
||||
async def test_empty_thread_works_correctly(self) -> None:
|
||||
"""Test that an empty thread (no message store) works correctly."""
|
||||
capturing_executor = ConversationHistoryCapturingExecutor(id="empty_thread_test")
|
||||
workflow = WorkflowBuilder().set_start_executor(capturing_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=capturing_executor).build()
|
||||
agent = WorkflowAgent(workflow=workflow, name="Empty Thread Test Agent")
|
||||
|
||||
# Create an empty thread
|
||||
@@ -597,7 +597,7 @@ class TestWorkflowAgent:
|
||||
from agent_framework import InMemoryCheckpointStorage
|
||||
|
||||
capturing_executor = ConversationHistoryCapturingExecutor(id="checkpoint_test")
|
||||
workflow = WorkflowBuilder().set_start_executor(capturing_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=capturing_executor).build()
|
||||
agent = WorkflowAgent(workflow=workflow, name="Checkpoint Test Agent")
|
||||
|
||||
# Create checkpoint storage
|
||||
@@ -675,17 +675,11 @@ class TestWorkflowAgent:
|
||||
await ctx.send_message(AgentExecutorRequest(messages=messages, should_respond=True))
|
||||
|
||||
# Build workflow: start -> agent1 (no output) -> agent2 (output_response=True)
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.register_executor(lambda: start_executor, "start")
|
||||
.register_agent(lambda: MockAgent("agent1", "Agent1 output - should NOT appear"), "agent1")
|
||||
.register_agent(lambda: MockAgent("agent2", "Agent2 output - SHOULD appear"), "agent2")
|
||||
.set_start_executor("start")
|
||||
.add_edge("start", "agent1")
|
||||
.add_edge("agent1", "agent2")
|
||||
.with_output_from(["start", "agent2"])
|
||||
.build()
|
||||
)
|
||||
builder = WorkflowBuilder(start_executor="start", output_executors=["start", "agent2"])
|
||||
builder.register_executor(lambda: start_executor, "start")
|
||||
builder.register_agent(lambda: MockAgent("agent1", "Agent1 output - should NOT appear"), "agent1")
|
||||
builder.register_agent(lambda: MockAgent("agent2", "Agent2 output - SHOULD appear"), "agent2")
|
||||
workflow = builder.add_edge("start", "agent1").add_edge("agent1", "agent2").build()
|
||||
|
||||
agent = WorkflowAgent(workflow=workflow, name="Test Agent")
|
||||
result = await agent.run("Test input")
|
||||
@@ -765,10 +759,9 @@ class TestWorkflowAgent:
|
||||
|
||||
# Build workflow with single agent
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
WorkflowBuilder(start_executor="start")
|
||||
.register_executor(lambda: start_executor, "start")
|
||||
.register_agent(lambda: MockAgent("agent", "Unique response text"), "agent")
|
||||
.set_start_executor("start")
|
||||
.add_edge("start", "agent")
|
||||
.build()
|
||||
)
|
||||
@@ -794,7 +787,7 @@ class TestWorkflowAgentAuthorName:
|
||||
"""
|
||||
# Create workflow with executor that emits AgentResponseUpdate without author_name
|
||||
executor1 = SimpleExecutor(id="my_executor_id", response_text="Response", streaming=True)
|
||||
workflow = WorkflowBuilder().set_start_executor(executor1).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).build()
|
||||
agent = WorkflowAgent(workflow=workflow, name="Test Agent")
|
||||
|
||||
# Collect streaming updates
|
||||
@@ -830,7 +823,7 @@ class TestWorkflowAgentAuthorName:
|
||||
await ctx.yield_output(update)
|
||||
|
||||
executor = AuthorNameExecutor(id="executor_id")
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
agent = WorkflowAgent(workflow=workflow, name="Test Agent")
|
||||
|
||||
# Collect streaming updates
|
||||
@@ -848,7 +841,7 @@ class TestWorkflowAgentAuthorName:
|
||||
executor1 = SimpleExecutor(id="first_executor", response_text="First")
|
||||
executor2 = SimpleExecutor(id="second_executor", response_text="Second")
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(executor1).add_edge(executor1, executor2).build()
|
||||
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
agent = WorkflowAgent(workflow=workflow, name="Multi-Executor Agent")
|
||||
|
||||
# Collect streaming updates
|
||||
|
||||
@@ -45,7 +45,7 @@ def test_builder_accepts_agents_directly():
|
||||
agent1 = DummyAgent(id="agent1", name="writer")
|
||||
agent2 = DummyAgent(id="agent2", name="reviewer")
|
||||
|
||||
wf = WorkflowBuilder().set_start_executor(agent1).add_edge(agent1, agent2).build()
|
||||
wf = WorkflowBuilder(start_executor=agent1).add_edge(agent1, agent2).build()
|
||||
|
||||
# Confirm auto-wrapped executors use agent names as IDs
|
||||
assert wf.start_executor_id == "writer"
|
||||
@@ -79,10 +79,8 @@ class MockAggregator(Executor):
|
||||
|
||||
def test_workflow_builder_without_start_executor_throws():
|
||||
"""Test creating a workflow builder without a start executor."""
|
||||
|
||||
builder = WorkflowBuilder()
|
||||
with pytest.raises(ValueError):
|
||||
builder.build()
|
||||
with pytest.raises(TypeError):
|
||||
WorkflowBuilder() # type: ignore[call-arg]
|
||||
|
||||
|
||||
def test_workflow_builder_fluent_api():
|
||||
@@ -95,13 +93,11 @@ def test_workflow_builder_fluent_api():
|
||||
executor_f = MockExecutor(id="executor_f")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
WorkflowBuilder(max_iterations=5, start_executor=executor_a)
|
||||
.add_edge(executor_a, executor_b)
|
||||
.add_fan_out_edges(executor_b, [executor_c, executor_d])
|
||||
.add_fan_in_edges([executor_c, executor_d], executor_e)
|
||||
.add_chain([executor_e, executor_f])
|
||||
.set_max_iterations(5)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -115,9 +111,8 @@ def test_add_agent_reuses_same_wrapper():
|
||||
reuse_agent = DummyAgent(id="agent_reuse", name="reuse_agent")
|
||||
agent_a = DummyAgent(id="agent_a", name="agent_a")
|
||||
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor=reuse_agent)
|
||||
# Use the same agent instance in add_edge - should reuse the same wrapper
|
||||
builder.set_start_executor(reuse_agent)
|
||||
builder.add_edge(reuse_agent, agent_a)
|
||||
builder.add_edge(agent_a, reuse_agent)
|
||||
|
||||
@@ -133,10 +128,10 @@ def test_add_agent_duplicate_id_raises_error():
|
||||
"""Test that adding agents with duplicate IDs raises an error."""
|
||||
agent1 = DummyAgent(id="agent1", name="first")
|
||||
agent2 = DummyAgent(id="agent2", name="first") # Same name as agent1
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor=agent1)
|
||||
|
||||
with pytest.raises(ValueError, match="Duplicate executor ID"):
|
||||
builder.set_start_executor(agent1).add_edge(agent1, agent2).build()
|
||||
builder.add_edge(agent1, agent2).build()
|
||||
|
||||
|
||||
# Tests for new executor registration patterns
|
||||
@@ -144,7 +139,7 @@ def test_add_agent_duplicate_id_raises_error():
|
||||
|
||||
def test_register_executor_basic():
|
||||
"""Test basic executor registration with lazy initialization."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="TestExecutor")
|
||||
|
||||
# Register an executor factory - ID must match the registered name
|
||||
result = builder.register_executor(lambda: MockExecutor(id="TestExecutor"), name="TestExecutor")
|
||||
@@ -153,14 +148,14 @@ def test_register_executor_basic():
|
||||
assert result is builder
|
||||
|
||||
# Build workflow and verify executor is instantiated
|
||||
workflow = builder.set_start_executor("TestExecutor").build()
|
||||
workflow = builder.build()
|
||||
assert "TestExecutor" in workflow.executors
|
||||
assert isinstance(workflow.executors["TestExecutor"], MockExecutor)
|
||||
|
||||
|
||||
def test_register_multiple_executors():
|
||||
"""Test registering multiple executors and connecting them with edges."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="ExecutorA")
|
||||
|
||||
# Register multiple executors - IDs must match registered names
|
||||
builder.register_executor(lambda: MockExecutor(id="ExecutorA"), name="ExecutorA")
|
||||
@@ -168,13 +163,7 @@ def test_register_multiple_executors():
|
||||
builder.register_executor(lambda: MockExecutor(id="ExecutorC"), name="ExecutorC")
|
||||
|
||||
# Build workflow with edges using registered names
|
||||
workflow = (
|
||||
builder
|
||||
.set_start_executor("ExecutorA")
|
||||
.add_edge("ExecutorA", "ExecutorB")
|
||||
.add_edge("ExecutorB", "ExecutorC")
|
||||
.build()
|
||||
)
|
||||
workflow = builder.add_edge("ExecutorA", "ExecutorB").add_edge("ExecutorB", "ExecutorC").build()
|
||||
|
||||
# Verify all executors are present
|
||||
assert "ExecutorA" in workflow.executors
|
||||
@@ -185,7 +174,7 @@ def test_register_multiple_executors():
|
||||
|
||||
def test_register_with_multiple_names():
|
||||
"""Test registering the same factory function under multiple names."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="ExecutorA")
|
||||
|
||||
# Register same executor factory under multiple names
|
||||
# Note: Each call creates a new instance, so IDs won't conflict
|
||||
@@ -198,7 +187,7 @@ def test_register_with_multiple_names():
|
||||
builder.register_executor(make_executor, name=["ExecutorA", "ExecutorB"])
|
||||
|
||||
# Set up workflow
|
||||
workflow = builder.set_start_executor("ExecutorA").add_edge("ExecutorA", "ExecutorB").build()
|
||||
workflow = builder.add_edge("ExecutorA", "ExecutorB").build()
|
||||
|
||||
# Verify both executors are present
|
||||
assert "ExecutorA" in workflow.executors
|
||||
@@ -208,7 +197,7 @@ def test_register_with_multiple_names():
|
||||
|
||||
def test_register_duplicate_name_raises_error():
|
||||
"""Test that registering duplicate names raises an error."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="MyExecutor")
|
||||
|
||||
# Register first executor
|
||||
builder.register_executor(lambda: MockExecutor(id="executor_1"), name="MyExecutor")
|
||||
@@ -220,12 +209,11 @@ def test_register_duplicate_name_raises_error():
|
||||
|
||||
def test_register_duplicate_id_raises_error():
|
||||
"""Test that registering duplicate id raises an error."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="MyExecutor1")
|
||||
|
||||
# Register first executor
|
||||
builder.register_executor(lambda: MockExecutor(id="executor"), name="MyExecutor1")
|
||||
builder.register_executor(lambda: MockExecutor(id="executor"), name="MyExecutor2")
|
||||
builder.set_start_executor("MyExecutor1")
|
||||
|
||||
# Registering second executor with same ID should raise ValueError
|
||||
with pytest.raises(ValueError, match="Executor with ID 'executor' has already been registered."):
|
||||
@@ -234,7 +222,7 @@ def test_register_duplicate_id_raises_error():
|
||||
|
||||
def test_register_agent_basic():
|
||||
"""Test basic agent registration with lazy initialization."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="TestAgent")
|
||||
|
||||
# Register an agent factory
|
||||
result = builder.register_agent(lambda: DummyAgent(id="agent_test", name="test_agent"), name="TestAgent")
|
||||
@@ -243,14 +231,14 @@ def test_register_agent_basic():
|
||||
assert result is builder
|
||||
|
||||
# Build workflow and verify agent is wrapped in AgentExecutor
|
||||
workflow = builder.set_start_executor("TestAgent").build()
|
||||
workflow = builder.build()
|
||||
assert "test_agent" in workflow.executors
|
||||
assert isinstance(workflow.executors["test_agent"], AgentExecutor)
|
||||
|
||||
|
||||
def test_register_agent_with_thread():
|
||||
"""Test registering an agent with a custom thread."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="ThreadedAgent")
|
||||
custom_thread = AgentThread()
|
||||
|
||||
# Register agent with custom thread
|
||||
@@ -261,7 +249,7 @@ def test_register_agent_with_thread():
|
||||
)
|
||||
|
||||
# Build workflow and verify agent executor configuration
|
||||
workflow = builder.set_start_executor("ThreadedAgent").build()
|
||||
workflow = builder.build()
|
||||
executor = workflow.executors["threaded_agent"]
|
||||
|
||||
assert isinstance(executor, AgentExecutor)
|
||||
@@ -271,7 +259,7 @@ def test_register_agent_with_thread():
|
||||
|
||||
def test_register_agent_duplicate_name_raises_error():
|
||||
"""Test that registering agents with duplicate names raises an error."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="MyAgent")
|
||||
|
||||
# Register first agent
|
||||
builder.register_agent(lambda: DummyAgent(id="agent1", name="first"), name="MyAgent")
|
||||
@@ -283,14 +271,14 @@ def test_register_agent_duplicate_name_raises_error():
|
||||
|
||||
def test_register_and_add_edge_with_strings():
|
||||
"""Test that registered executors can be connected using string names."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="Source")
|
||||
|
||||
# Register executors
|
||||
builder.register_executor(lambda: MockExecutor(id="source"), name="Source")
|
||||
builder.register_executor(lambda: MockExecutor(id="target"), name="Target")
|
||||
|
||||
# Add edge using string names
|
||||
workflow = builder.set_start_executor("Source").add_edge("Source", "Target").build()
|
||||
workflow = builder.add_edge("Source", "Target").build()
|
||||
|
||||
# Verify edge is created correctly
|
||||
assert workflow.start_executor_id == "source"
|
||||
@@ -300,14 +288,14 @@ def test_register_and_add_edge_with_strings():
|
||||
|
||||
def test_register_agent_and_add_edge_with_strings():
|
||||
"""Test that registered agents can be connected using string names."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="Writer")
|
||||
|
||||
# Register agents
|
||||
builder.register_agent(lambda: DummyAgent(id="writer_id", name="writer"), name="Writer")
|
||||
builder.register_agent(lambda: DummyAgent(id="reviewer_id", name="reviewer"), name="Reviewer")
|
||||
|
||||
# Add edge using string names
|
||||
workflow = builder.set_start_executor("Writer").add_edge("Writer", "Reviewer").build()
|
||||
workflow = builder.add_edge("Writer", "Reviewer").build()
|
||||
|
||||
# Verify edge is created correctly
|
||||
assert workflow.start_executor_id == "writer"
|
||||
@@ -318,7 +306,7 @@ def test_register_agent_and_add_edge_with_strings():
|
||||
|
||||
def test_register_with_fan_out_edges():
|
||||
"""Test using registered names with fan-out edge groups."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="Source")
|
||||
|
||||
# Register executors - IDs must match registered names
|
||||
builder.register_executor(lambda: MockExecutor(id="Source"), name="Source")
|
||||
@@ -326,7 +314,7 @@ def test_register_with_fan_out_edges():
|
||||
builder.register_executor(lambda: MockExecutor(id="Target2"), name="Target2")
|
||||
|
||||
# Add fan-out edges using registered names
|
||||
workflow = builder.set_start_executor("Source").add_fan_out_edges("Source", ["Target1", "Target2"]).build()
|
||||
workflow = builder.add_fan_out_edges("Source", ["Target1", "Target2"]).build()
|
||||
|
||||
# Verify all executors are present
|
||||
assert "Source" in workflow.executors
|
||||
@@ -336,7 +324,7 @@ def test_register_with_fan_out_edges():
|
||||
|
||||
def test_register_with_fan_in_edges():
|
||||
"""Test using registered names with fan-in edge groups."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="Source1")
|
||||
|
||||
# Register executors - IDs must match registered names
|
||||
builder.register_executor(lambda: MockExecutor(id="Source1"), name="Source1")
|
||||
@@ -345,13 +333,7 @@ def test_register_with_fan_in_edges():
|
||||
|
||||
# Add fan-in edges using registered names
|
||||
# Both Source1 and Source2 need to be reachable, so connect Source1 to Source2
|
||||
workflow = (
|
||||
builder
|
||||
.set_start_executor("Source1")
|
||||
.add_edge("Source1", "Source2")
|
||||
.add_fan_in_edges(["Source1", "Source2"], "Aggregator")
|
||||
.build()
|
||||
)
|
||||
workflow = builder.add_edge("Source1", "Source2").add_fan_in_edges(["Source1", "Source2"], "Aggregator").build()
|
||||
|
||||
# Verify all executors are present
|
||||
assert "Source1" in workflow.executors
|
||||
@@ -361,7 +343,7 @@ def test_register_with_fan_in_edges():
|
||||
|
||||
def test_register_with_chain():
|
||||
"""Test using registered names with add_chain."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="Step1")
|
||||
|
||||
# Register executors - IDs must match registered names
|
||||
builder.register_executor(lambda: MockExecutor(id="Step1"), name="Step1")
|
||||
@@ -369,7 +351,7 @@ def test_register_with_chain():
|
||||
builder.register_executor(lambda: MockExecutor(id="Step3"), name="Step3")
|
||||
|
||||
# Add chain using registered names
|
||||
workflow = builder.add_chain(["Step1", "Step2", "Step3"]).set_start_executor("Step1").build()
|
||||
workflow = builder.add_chain(["Step1", "Step2", "Step3"]).build()
|
||||
|
||||
# Verify all executors are present
|
||||
assert "Step1" in workflow.executors
|
||||
@@ -387,15 +369,12 @@ def test_register_factory_called_only_once():
|
||||
call_count += 1
|
||||
return MockExecutor(id="Test")
|
||||
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="Test")
|
||||
builder.register_executor(factory, name="Test")
|
||||
|
||||
# Factory should not be called yet
|
||||
assert call_count == 0
|
||||
|
||||
# Add edge without building
|
||||
builder.set_start_executor("Test")
|
||||
|
||||
# Factory should still not be called
|
||||
assert call_count == 0
|
||||
|
||||
@@ -409,7 +388,7 @@ def test_register_factory_called_only_once():
|
||||
|
||||
def test_mixing_eager_and_lazy_initialization_error():
|
||||
"""Test that mixing eager executor instances with lazy string names raises appropriate error."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="Lazy")
|
||||
|
||||
# Create an eager executor instance
|
||||
eager_executor = MockExecutor(id="eager")
|
||||
@@ -430,7 +409,7 @@ def test_mixing_eager_and_lazy_initialization_error():
|
||||
|
||||
def test_register_with_condition():
|
||||
"""Test adding edges with conditions using registered names."""
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(start_executor="Source")
|
||||
|
||||
def condition_func(msg: MockMessage) -> bool:
|
||||
return msg.data > 0
|
||||
@@ -440,7 +419,7 @@ def test_register_with_condition():
|
||||
builder.register_executor(lambda: MockExecutor(id="Target"), name="Target")
|
||||
|
||||
# Add edge with condition
|
||||
workflow = builder.set_start_executor("Source").add_edge("Source", "Target", condition=condition_func).build()
|
||||
workflow = builder.add_edge("Source", "Target", condition=condition_func).build()
|
||||
|
||||
# Verify workflow is built correctly
|
||||
assert "Source" in workflow.executors
|
||||
@@ -457,14 +436,14 @@ def test_register_agent_creates_unique_instances():
|
||||
return agent
|
||||
|
||||
# Build first workflow
|
||||
builder1 = WorkflowBuilder()
|
||||
builder1 = WorkflowBuilder(start_executor="Agent")
|
||||
builder1.register_agent(agent_factory, name="Agent")
|
||||
_ = builder1.set_start_executor("Agent").build()
|
||||
_ = builder1.build()
|
||||
|
||||
# Build second workflow
|
||||
builder2 = WorkflowBuilder()
|
||||
builder2 = WorkflowBuilder(start_executor="Agent")
|
||||
builder2.register_agent(agent_factory, name="Agent")
|
||||
_ = builder2.set_start_executor("Agent").build()
|
||||
_ = builder2.build()
|
||||
|
||||
# Verify that two different agent instances were created
|
||||
assert len(instance_ids) == 2
|
||||
@@ -477,11 +456,10 @@ def test_register_agent_creates_unique_instances():
|
||||
def test_with_output_from_returns_builder():
|
||||
"""Test that with_output_from returns the builder for method chaining."""
|
||||
executor_a = MockExecutor(id="executor_a")
|
||||
builder = WorkflowBuilder()
|
||||
builder = WorkflowBuilder(output_executors=[executor_a], start_executor=executor_a)
|
||||
|
||||
result = builder.with_output_from([executor_a])
|
||||
|
||||
assert result is builder
|
||||
# Verify builder was created with output_executors
|
||||
assert builder._output_executors == [executor_a]
|
||||
|
||||
|
||||
def test_with_output_from_with_executor_instances():
|
||||
@@ -490,10 +468,8 @@ def test_with_output_from_with_executor_instances():
|
||||
executor_b = MockExecutor(id="executor_b")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_b])
|
||||
.add_edge(executor_a, executor_b)
|
||||
.with_output_from([executor_b])
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -506,9 +482,7 @@ def test_with_output_from_with_agent_instances():
|
||||
agent_a = DummyAgent(id="agent_a", name="writer")
|
||||
agent_b = DummyAgent(id="agent_b", name="reviewer")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder().set_start_executor(agent_a).add_edge(agent_a, agent_b).with_output_from([agent_b]).build()
|
||||
)
|
||||
workflow = WorkflowBuilder(start_executor=agent_a, output_executors=[agent_b]).add_edge(agent_a, agent_b).build()
|
||||
|
||||
# Verify that the workflow was built with the agent's name as output executor
|
||||
assert workflow._output_executors == ["reviewer"] # type: ignore
|
||||
@@ -516,15 +490,10 @@ def test_with_output_from_with_agent_instances():
|
||||
|
||||
def test_with_output_from_with_registered_names():
|
||||
"""Test with_output_from with registered factory names (strings)."""
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.register_executor(lambda: MockExecutor(id="ExecutorA"), name="ExecutorAFactory")
|
||||
.register_executor(lambda: MockExecutor(id="ExecutorB"), name="ExecutorBFactory")
|
||||
.set_start_executor("ExecutorAFactory")
|
||||
.add_edge("ExecutorAFactory", "ExecutorBFactory")
|
||||
.with_output_from(["ExecutorBFactory"])
|
||||
.build()
|
||||
)
|
||||
builder = WorkflowBuilder(start_executor="ExecutorAFactory", output_executors=["ExecutorBFactory"])
|
||||
builder.register_executor(lambda: MockExecutor(id="ExecutorA"), name="ExecutorAFactory")
|
||||
builder.register_executor(lambda: MockExecutor(id="ExecutorB"), name="ExecutorBFactory")
|
||||
workflow = builder.add_edge("ExecutorAFactory", "ExecutorBFactory").build()
|
||||
|
||||
# Verify that the workflow was built with the correct output executors
|
||||
assert workflow._output_executors == ["ExecutorB"] # type: ignore
|
||||
@@ -537,11 +506,9 @@ def test_with_output_from_with_multiple_executors():
|
||||
executor_c = MockExecutor(id="executor_c")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_a, executor_c])
|
||||
.add_edge(executor_a, executor_b)
|
||||
.add_edge(executor_b, executor_c)
|
||||
.with_output_from([executor_a, executor_c])
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -549,51 +516,41 @@ def test_with_output_from_with_multiple_executors():
|
||||
assert set(workflow._output_executors) == {"executor_a", "executor_c"} # type: ignore
|
||||
|
||||
|
||||
def test_with_output_from_can_be_called_multiple_times():
|
||||
"""Test that calling with_output_from multiple times overwrites the previous setting."""
|
||||
def test_with_output_from_can_be_set_to_different_value():
|
||||
"""Test that output_executors can be set at construction time."""
|
||||
executor_a = MockExecutor(id="executor_a")
|
||||
executor_b = MockExecutor(id="executor_b")
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_b])
|
||||
.add_edge(executor_a, executor_b)
|
||||
.with_output_from([executor_a])
|
||||
.with_output_from([executor_b]) # This should overwrite the previous setting
|
||||
.build()
|
||||
)
|
||||
|
||||
# Verify that only the last setting is applied
|
||||
# Verify that the setting is applied
|
||||
assert workflow._output_executors == ["executor_b"] # type: ignore
|
||||
|
||||
|
||||
def test_with_output_from_with_registered_agents():
|
||||
"""Test with_output_from with registered agent factory names."""
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.register_agent(lambda: DummyAgent(id="agent1", name="writer"), name="WriterAgent")
|
||||
.register_agent(lambda: DummyAgent(id="agent2", name="reviewer"), name="ReviewerAgent")
|
||||
.set_start_executor("WriterAgent")
|
||||
.add_edge("WriterAgent", "ReviewerAgent")
|
||||
.with_output_from(["ReviewerAgent"])
|
||||
.build()
|
||||
)
|
||||
builder = WorkflowBuilder(start_executor="WriterAgent", output_executors=["ReviewerAgent"])
|
||||
builder.register_agent(lambda: DummyAgent(id="agent1", name="writer"), name="WriterAgent")
|
||||
builder.register_agent(lambda: DummyAgent(id="agent2", name="reviewer"), name="ReviewerAgent")
|
||||
workflow = builder.add_edge("WriterAgent", "ReviewerAgent").build()
|
||||
|
||||
# Verify that the workflow was built with the agent's resolved name
|
||||
assert workflow._output_executors == ["reviewer"] # type: ignore
|
||||
|
||||
|
||||
def test_with_output_from_in_fluent_chain():
|
||||
"""Test that with_output_from works correctly in a fluent builder chain."""
|
||||
def test_with_output_from_in_constructor():
|
||||
"""Test that output_executors works correctly when set in the constructor."""
|
||||
executor_a = MockExecutor(id="executor_a")
|
||||
executor_b = MockExecutor(id="executor_b")
|
||||
executor_c = MockExecutor(id="executor_c")
|
||||
|
||||
# Build workflow with with_output_from in the middle of the chain
|
||||
# Build workflow with output_executors in the constructor
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor_a)
|
||||
.with_output_from([executor_c]) # Set early in the chain
|
||||
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_c])
|
||||
.add_edge(executor_a, executor_b)
|
||||
.add_edge(executor_b, executor_c)
|
||||
.build()
|
||||
@@ -607,13 +564,13 @@ def test_with_output_from_with_invalid_executor_raises_validation_error():
|
||||
"""Test that with_output_from with an invalid executor raises an error."""
|
||||
executor_a = MockExecutor(id="executor_a")
|
||||
|
||||
builder = WorkflowBuilder().set_start_executor(executor_a)
|
||||
builder = WorkflowBuilder(start_executor=executor_a, output_executors=[MockExecutor(id="executor_b")])
|
||||
|
||||
# Attempting to set output from an executor not in the workflow should raise an error
|
||||
with pytest.raises(
|
||||
WorkflowValidationError, match="Output executor 'executor_b' is not present in the workflow graph"
|
||||
):
|
||||
builder.with_output_from([MockExecutor(id="executor_b")]).build()
|
||||
builder.build()
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -93,7 +93,7 @@ async def test_workflow_context_type_annotations_no_parameter() -> None:
|
||||
async def func1(text: str, ctx: WorkflowContext) -> None:
|
||||
await ctx.add_event(_TestEvent())
|
||||
|
||||
wf = WorkflowBuilder().set_start_executor(func1).build()
|
||||
wf = WorkflowBuilder(start_executor=func1).build()
|
||||
events = await wf.run("hello")
|
||||
test_events = [e for e in events if isinstance(e, _TestEvent)]
|
||||
assert len(test_events) == 1
|
||||
@@ -110,7 +110,7 @@ async def test_workflow_context_type_annotations_no_parameter() -> None:
|
||||
assert executor1.output_types == []
|
||||
assert executor1.workflow_output_types == []
|
||||
|
||||
wf2 = WorkflowBuilder().set_start_executor(executor1).build()
|
||||
wf2 = WorkflowBuilder(start_executor=executor1).build()
|
||||
events2 = await wf2.run("hello")
|
||||
test_events2 = [e for e in events2 if isinstance(e, _TestEvent)]
|
||||
assert len(test_events2) == 1
|
||||
@@ -126,7 +126,7 @@ async def test_workflow_context_type_annotations_message_type_parameter() -> Non
|
||||
async def func2(text: str, ctx: WorkflowContext) -> None:
|
||||
await ctx.add_event(_TestEvent(data=text))
|
||||
|
||||
wf = WorkflowBuilder().add_edge(func1, func2).set_start_executor(func1).build()
|
||||
wf = WorkflowBuilder(start_executor=func1).add_edge(func1, func2).build()
|
||||
events = await wf.run("hello")
|
||||
test_events = [e for e in events if isinstance(e, _TestEvent)]
|
||||
assert len(test_events) == 1
|
||||
@@ -153,7 +153,7 @@ async def test_workflow_context_type_annotations_message_type_parameter() -> Non
|
||||
assert executor2.output_types == []
|
||||
assert executor2.workflow_output_types == []
|
||||
|
||||
wf2 = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
wf2 = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
events2 = await wf2.run("hello")
|
||||
test_events2 = [e for e in events2 if isinstance(e, _TestEvent)]
|
||||
assert len(test_events2) == 1
|
||||
@@ -171,7 +171,7 @@ async def test_workflow_context_type_annotations_message_and_output_type_paramet
|
||||
await ctx.add_event(_TestEvent(data=text))
|
||||
await ctx.yield_output(text)
|
||||
|
||||
wf = WorkflowBuilder().add_edge(func1, func2).set_start_executor(func1).build()
|
||||
wf = WorkflowBuilder(start_executor=func1).add_edge(func1, func2).build()
|
||||
events = await wf.run("hello")
|
||||
outputs = events.get_outputs()
|
||||
assert len(outputs) == 1
|
||||
@@ -199,7 +199,7 @@ async def test_workflow_context_type_annotations_message_and_output_type_paramet
|
||||
assert executor2.output_types == []
|
||||
assert executor2.workflow_output_types == [str]
|
||||
|
||||
wf2 = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
|
||||
wf2 = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
|
||||
events2 = await wf2.run("hello")
|
||||
outputs2 = events2.get_outputs()
|
||||
assert len(outputs2) == 1
|
||||
|
||||
@@ -78,7 +78,7 @@ class _KwargsCapturingAgent(BaseAgent):
|
||||
async def test_sequential_kwargs_flow_to_agent() -> None:
|
||||
"""Test that kwargs passed to SequentialBuilder workflow flow through to agent."""
|
||||
agent = _KwargsCapturingAgent(name="seq_agent")
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
workflow = SequentialBuilder(participants=[agent]).build()
|
||||
|
||||
custom_data = {"endpoint": "https://api.example.com", "version": "v1"}
|
||||
user_token = {"user_name": "alice", "access_level": "admin"}
|
||||
@@ -105,7 +105,7 @@ async def test_sequential_kwargs_flow_to_multiple_agents() -> None:
|
||||
"""Test that kwargs flow to all agents in a sequential workflow."""
|
||||
agent1 = _KwargsCapturingAgent(name="agent1")
|
||||
agent2 = _KwargsCapturingAgent(name="agent2")
|
||||
workflow = SequentialBuilder().participants([agent1, agent2]).build()
|
||||
workflow = SequentialBuilder(participants=[agent1, agent2]).build()
|
||||
|
||||
custom_data = {"key": "value"}
|
||||
|
||||
@@ -123,7 +123,7 @@ async def test_sequential_kwargs_flow_to_multiple_agents() -> None:
|
||||
async def test_sequential_run_kwargs_flow() -> None:
|
||||
"""Test that kwargs flow through workflow.run() (non-streaming)."""
|
||||
agent = _KwargsCapturingAgent(name="run_agent")
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
workflow = SequentialBuilder(participants=[agent]).build()
|
||||
|
||||
_ = await workflow.run("test message", custom_data={"test": True})
|
||||
|
||||
@@ -141,7 +141,7 @@ async def test_concurrent_kwargs_flow_to_agents() -> None:
|
||||
"""Test that kwargs flow to all agents in a concurrent workflow."""
|
||||
agent1 = _KwargsCapturingAgent(name="concurrent1")
|
||||
agent2 = _KwargsCapturingAgent(name="concurrent2")
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2]).build()
|
||||
workflow = ConcurrentBuilder(participants=[agent1, agent2]).build()
|
||||
|
||||
custom_data = {"batch_id": "123"}
|
||||
user_token = {"user_name": "bob"}
|
||||
@@ -188,13 +188,11 @@ async def test_groupchat_kwargs_flow_to_agents() -> None:
|
||||
names = list(state.participants.keys())
|
||||
return names[(turn_count - 1) % len(names)]
|
||||
|
||||
workflow = (
|
||||
GroupChatBuilder()
|
||||
.participants([agent1, agent2])
|
||||
.with_orchestrator(selection_func=simple_selector)
|
||||
.with_max_rounds(2) # Limit rounds to prevent infinite loop
|
||||
.build()
|
||||
)
|
||||
workflow = GroupChatBuilder(
|
||||
participants=[agent1, agent2],
|
||||
max_rounds=2, # Limit rounds to prevent infinite loop
|
||||
selection_func=simple_selector,
|
||||
).build()
|
||||
|
||||
custom_data = {"session_id": "group123"}
|
||||
|
||||
@@ -230,7 +228,7 @@ async def test_kwargs_stored_in_state() -> None:
|
||||
await ctx.send_message(msgs)
|
||||
|
||||
inspector = _StateInspector(id="inspector")
|
||||
workflow = SequentialBuilder().participants([inspector]).build()
|
||||
workflow = SequentialBuilder(participants=[inspector]).build()
|
||||
|
||||
async for event in workflow.run("test", my_kwarg="my_value", another=123, stream=True):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
@@ -255,7 +253,7 @@ async def test_empty_kwargs_stored_as_empty_dict() -> None:
|
||||
await ctx.send_message(msgs)
|
||||
|
||||
checker = _StateChecker(id="checker")
|
||||
workflow = SequentialBuilder().participants([checker]).build()
|
||||
workflow = SequentialBuilder(participants=[checker]).build()
|
||||
|
||||
# Run without any kwargs
|
||||
async for event in workflow.run("test", stream=True):
|
||||
@@ -275,7 +273,7 @@ async def test_empty_kwargs_stored_as_empty_dict() -> None:
|
||||
async def test_kwargs_with_none_values() -> None:
|
||||
"""Test that kwargs with None values are passed through correctly."""
|
||||
agent = _KwargsCapturingAgent(name="none_test")
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
workflow = SequentialBuilder(participants=[agent]).build()
|
||||
|
||||
async for event in workflow.run("test", optional_param=None, other_param="value", stream=True):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
@@ -291,7 +289,7 @@ async def test_kwargs_with_none_values() -> None:
|
||||
async def test_kwargs_with_complex_nested_data() -> None:
|
||||
"""Test that complex nested data structures flow through correctly."""
|
||||
agent = _KwargsCapturingAgent(name="nested_test")
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
workflow = SequentialBuilder(participants=[agent]).build()
|
||||
|
||||
complex_data = {
|
||||
"level1": {
|
||||
@@ -318,8 +316,8 @@ async def test_kwargs_preserved_across_workflow_reruns() -> None:
|
||||
agent = _KwargsCapturingAgent(name="rerun_test")
|
||||
|
||||
# Build separate workflows for each run to avoid "already running" error
|
||||
workflow1 = SequentialBuilder().participants([agent]).build()
|
||||
workflow2 = SequentialBuilder().participants([agent]).build()
|
||||
workflow1 = SequentialBuilder(participants=[agent]).build()
|
||||
workflow2 = SequentialBuilder(participants=[agent]).build()
|
||||
|
||||
# First run
|
||||
async for event in workflow1.run("run1", run_id="first", stream=True):
|
||||
@@ -349,11 +347,10 @@ async def test_handoff_kwargs_flow_to_agents() -> None:
|
||||
agent2 = _KwargsCapturingAgent(name="specialist")
|
||||
|
||||
workflow = (
|
||||
HandoffBuilder()
|
||||
HandoffBuilder(termination_condition=lambda conv: len(conv) >= 4)
|
||||
.participants([agent1, agent2])
|
||||
.with_start_agent(agent1)
|
||||
.with_autonomous_mode()
|
||||
.with_termination_condition(lambda conv: len(conv) >= 4)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -413,7 +410,7 @@ async def test_magentic_kwargs_flow_to_agents() -> None:
|
||||
agent = _KwargsCapturingAgent(name="agent1")
|
||||
manager = _MockManager()
|
||||
|
||||
workflow = MagenticBuilder().participants([agent]).with_manager(manager=manager).build()
|
||||
workflow = MagenticBuilder(participants=[agent], manager=manager).build()
|
||||
|
||||
custom_data = {"session_id": "magentic123"}
|
||||
|
||||
@@ -463,7 +460,7 @@ async def test_magentic_kwargs_stored_in_state() -> None:
|
||||
agent = _KwargsCapturingAgent(name="agent1")
|
||||
manager = _MockManager()
|
||||
|
||||
magentic_workflow = MagenticBuilder().participants([agent]).with_manager(manager=manager).build()
|
||||
magentic_workflow = MagenticBuilder(participants=[agent], manager=manager).build()
|
||||
|
||||
# Use MagenticWorkflow.run() which goes through the kwargs attachment path
|
||||
custom_data = {"magentic_key": "magentic_value"}
|
||||
@@ -485,7 +482,7 @@ async def test_magentic_kwargs_stored_in_state() -> None:
|
||||
async def test_workflow_as_agent_run_propagates_kwargs_to_underlying_agent() -> None:
|
||||
"""Test that kwargs passed to workflow_agent.run() flow through to the underlying agents."""
|
||||
agent = _KwargsCapturingAgent(name="inner_agent")
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
workflow = SequentialBuilder(participants=[agent]).build()
|
||||
workflow_agent = workflow.as_agent(name="TestWorkflowAgent")
|
||||
|
||||
custom_data = {"endpoint": "https://api.example.com", "version": "v1"}
|
||||
@@ -509,7 +506,7 @@ async def test_workflow_as_agent_run_propagates_kwargs_to_underlying_agent() ->
|
||||
async def test_workflow_as_agent_run_stream_propagates_kwargs_to_underlying_agent() -> None:
|
||||
"""Test that kwargs passed to workflow_agent.run() flow through to the underlying agents."""
|
||||
agent = _KwargsCapturingAgent(name="inner_agent")
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
workflow = SequentialBuilder(participants=[agent]).build()
|
||||
workflow_agent = workflow.as_agent(name="TestWorkflowAgent")
|
||||
|
||||
custom_data = {"session_id": "xyz123"}
|
||||
@@ -536,7 +533,7 @@ async def test_workflow_as_agent_propagates_kwargs_to_multiple_agents() -> None:
|
||||
"""Test that kwargs flow to all agents when using workflow.as_agent()."""
|
||||
agent1 = _KwargsCapturingAgent(name="agent1")
|
||||
agent2 = _KwargsCapturingAgent(name="agent2")
|
||||
workflow = SequentialBuilder().participants([agent1, agent2]).build()
|
||||
workflow = SequentialBuilder(participants=[agent1, agent2]).build()
|
||||
workflow_agent = workflow.as_agent(name="MultiAgentWorkflow")
|
||||
|
||||
custom_data = {"batch_id": "batch-001"}
|
||||
@@ -553,7 +550,7 @@ async def test_workflow_as_agent_propagates_kwargs_to_multiple_agents() -> None:
|
||||
async def test_workflow_as_agent_kwargs_with_none_values() -> None:
|
||||
"""Test that kwargs with None values are passed through correctly via as_agent()."""
|
||||
agent = _KwargsCapturingAgent(name="none_test_agent")
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
workflow = SequentialBuilder(participants=[agent]).build()
|
||||
workflow_agent = workflow.as_agent(name="NoneTestWorkflow")
|
||||
|
||||
_ = await workflow_agent.run("test", optional_param=None, other_param="value")
|
||||
@@ -568,7 +565,7 @@ async def test_workflow_as_agent_kwargs_with_none_values() -> None:
|
||||
async def test_workflow_as_agent_kwargs_with_complex_nested_data() -> None:
|
||||
"""Test that complex nested data structures flow through correctly via as_agent()."""
|
||||
agent = _KwargsCapturingAgent(name="nested_agent")
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
workflow = SequentialBuilder(participants=[agent]).build()
|
||||
workflow_agent = workflow.as_agent(name="NestedDataWorkflow")
|
||||
|
||||
complex_data = {
|
||||
@@ -606,13 +603,13 @@ async def test_subworkflow_kwargs_propagation() -> None:
|
||||
inner_agent = _KwargsCapturingAgent(name="inner_agent")
|
||||
|
||||
# Build the inner (sub) workflow with the agent
|
||||
inner_workflow = SequentialBuilder().participants([inner_agent]).build()
|
||||
inner_workflow = SequentialBuilder(participants=[inner_agent]).build()
|
||||
|
||||
# Wrap the inner workflow in a WorkflowExecutor so it can be used as a subworkflow
|
||||
subworkflow_executor = WorkflowExecutor(workflow=inner_workflow, id="subworkflow_executor")
|
||||
|
||||
# Build the outer (parent) workflow containing the subworkflow
|
||||
outer_workflow = SequentialBuilder().participants([subworkflow_executor]).build()
|
||||
outer_workflow = SequentialBuilder(participants=[subworkflow_executor]).build()
|
||||
|
||||
# Define kwargs that should propagate to subworkflow
|
||||
custom_data = {"api_key": "secret123", "endpoint": "https://api.example.com"}
|
||||
@@ -670,13 +667,13 @@ async def test_subworkflow_kwargs_accessible_via_state() -> None:
|
||||
|
||||
# Build inner workflow with State reader
|
||||
state_reader = _StateReader(id="state_reader")
|
||||
inner_workflow = SequentialBuilder().participants([state_reader]).build()
|
||||
inner_workflow = SequentialBuilder(participants=[state_reader]).build()
|
||||
|
||||
# Wrap as subworkflow
|
||||
subworkflow_executor = WorkflowExecutor(workflow=inner_workflow, id="subworkflow")
|
||||
|
||||
# Build outer workflow
|
||||
outer_workflow = SequentialBuilder().participants([subworkflow_executor]).build()
|
||||
outer_workflow = SequentialBuilder(participants=[subworkflow_executor]).build()
|
||||
|
||||
# Run with kwargs
|
||||
async for event in outer_workflow.run(
|
||||
@@ -715,15 +712,15 @@ async def test_nested_subworkflow_kwargs_propagation() -> None:
|
||||
inner_agent = _KwargsCapturingAgent(name="deeply_nested_agent")
|
||||
|
||||
# Build inner workflow
|
||||
inner_workflow = SequentialBuilder().participants([inner_agent]).build()
|
||||
inner_workflow = SequentialBuilder(participants=[inner_agent]).build()
|
||||
inner_executor = WorkflowExecutor(workflow=inner_workflow, id="inner_executor")
|
||||
|
||||
# Build middle workflow containing inner
|
||||
middle_workflow = SequentialBuilder().participants([inner_executor]).build()
|
||||
middle_workflow = SequentialBuilder(participants=[inner_executor]).build()
|
||||
middle_executor = WorkflowExecutor(workflow=middle_workflow, id="middle_executor")
|
||||
|
||||
# Build outer workflow containing middle
|
||||
outer_workflow = SequentialBuilder().participants([middle_executor]).build()
|
||||
outer_workflow = SequentialBuilder(participants=[middle_executor]).build()
|
||||
|
||||
# Run with kwargs
|
||||
async for event in outer_workflow.run(
|
||||
|
||||
@@ -268,8 +268,7 @@ async def test_end_to_end_workflow_tracing(span_exporter: InMemorySpanExporter)
|
||||
|
||||
# Create workflow with fan-in: executor1 -> [executor2, executor3] -> aggregator
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(executor1)
|
||||
WorkflowBuilder(start_executor=executor1)
|
||||
.add_fan_out_edges(executor1, [executor2, executor3])
|
||||
.add_fan_in_edges([executor2, executor3], aggregator)
|
||||
.build()
|
||||
@@ -297,11 +296,11 @@ async def test_end_to_end_workflow_tracing(span_exporter: InMemorySpanExporter)
|
||||
span_exporter.clear()
|
||||
|
||||
# Test workflow with name and description - verify OTEL attributes
|
||||
(
|
||||
WorkflowBuilder(name="Test Pipeline", description="Test workflow description")
|
||||
.set_start_executor(MockExecutor("start"))
|
||||
.build()
|
||||
)
|
||||
WorkflowBuilder(
|
||||
name="Test Pipeline",
|
||||
description="Test workflow description",
|
||||
start_executor=MockExecutor("start"),
|
||||
).build()
|
||||
|
||||
build_spans_with_metadata = [s for s in span_exporter.get_finished_spans() if s.name == "workflow.build"]
|
||||
assert len(build_spans_with_metadata) == 1
|
||||
@@ -412,7 +411,7 @@ async def test_workflow_error_handling_in_tracing(span_exporter: InMemorySpanExp
|
||||
raise ValueError("Test error")
|
||||
|
||||
failing_executor = FailingExecutor()
|
||||
workflow = WorkflowBuilder().set_start_executor(failing_executor).build()
|
||||
workflow = WorkflowBuilder(start_executor=failing_executor).build()
|
||||
|
||||
# Run workflow and expect error
|
||||
with pytest.raises(ValueError, match="Test error"):
|
||||
@@ -475,10 +474,10 @@ async def test_message_trace_context_serialization(span_exporter: InMemorySpanEx
|
||||
async def test_workflow_build_error_tracing(span_exporter: InMemorySpanExporter) -> None:
|
||||
"""Test that build errors are properly recorded in build spans."""
|
||||
|
||||
# Test validation error by not setting start executor
|
||||
builder = WorkflowBuilder()
|
||||
# Test validation error by referencing a non-existent start executor
|
||||
builder = WorkflowBuilder(start_executor="NonExistent")
|
||||
|
||||
with pytest.raises(ValueError, match="Starting executor must be set"):
|
||||
with pytest.raises(ValueError):
|
||||
builder.build()
|
||||
|
||||
spans = span_exporter.get_finished_spans()
|
||||
@@ -501,5 +500,5 @@ async def test_workflow_build_error_tracing(span_exporter: InMemorySpanExporter)
|
||||
|
||||
error_event = error_events[0]
|
||||
assert error_event.attributes is not None
|
||||
assert "Starting executor must be set" in str(error_event.attributes.get("build.error.message"))
|
||||
assert "starting executor" in str(error_event.attributes.get("build.error.message")).lower()
|
||||
assert error_event.attributes.get("build.error.type") == "ValueError"
|
||||
|
||||
@@ -28,7 +28,7 @@ class FailingExecutor(Executor):
|
||||
|
||||
async def test_executor_failed_and_workflow_failed_events_streaming():
|
||||
failing = FailingExecutor(id="f")
|
||||
wf: Workflow = WorkflowBuilder().set_start_executor(failing).build()
|
||||
wf: Workflow = WorkflowBuilder(start_executor=failing).build()
|
||||
|
||||
events: list[object] = []
|
||||
with pytest.raises(RuntimeError, match="boom"):
|
||||
@@ -86,7 +86,7 @@ async def test_executor_failed_event_from_second_executor_in_chain():
|
||||
"""Test that executor_failed event is emitted when a non-start executor fails."""
|
||||
passthrough = PassthroughExecutor(id="passthrough")
|
||||
failing = FailingExecutor(id="failing")
|
||||
wf: Workflow = WorkflowBuilder().set_start_executor(passthrough).add_edge(passthrough, failing).build()
|
||||
wf: Workflow = WorkflowBuilder(start_executor=passthrough).add_edge(passthrough, failing).build()
|
||||
|
||||
events: list[object] = []
|
||||
with pytest.raises(RuntimeError, match="boom"):
|
||||
@@ -131,7 +131,7 @@ class Requester(Executor):
|
||||
async def test_idle_with_pending_requests_status_streaming():
|
||||
simple_executor = SimpleExecutor(id="simple")
|
||||
requester = Requester(id="req")
|
||||
wf = WorkflowBuilder().set_start_executor(simple_executor).add_edge(simple_executor, requester).build()
|
||||
wf = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, requester).build()
|
||||
|
||||
events = [ev async for ev in wf.run("start", stream=True)] # Consume stream fully
|
||||
|
||||
@@ -153,7 +153,7 @@ class Completer(Executor):
|
||||
|
||||
async def test_completed_status_streaming():
|
||||
c = Completer(id="c")
|
||||
wf = WorkflowBuilder().set_start_executor(c).build()
|
||||
wf = WorkflowBuilder(start_executor=c).build()
|
||||
events = [ev async for ev in wf.run("ok", stream=True)] # no raise
|
||||
# Last status should be IDLE
|
||||
status = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "status"]
|
||||
@@ -163,7 +163,7 @@ async def test_completed_status_streaming():
|
||||
|
||||
async def test_started_and_completed_event_origins():
|
||||
c = Completer(id="c-origin")
|
||||
wf = WorkflowBuilder().set_start_executor(c).build()
|
||||
wf = WorkflowBuilder(start_executor=c).build()
|
||||
events = [ev async for ev in wf.run("payload", stream=True)]
|
||||
|
||||
started = next(e for e in events if isinstance(e, WorkflowEvent) and e.type == "started")
|
||||
@@ -181,21 +181,21 @@ async def test_started_and_completed_event_origins():
|
||||
async def test_non_streaming_final_state_helpers():
|
||||
# Completed case
|
||||
c = Completer(id="c")
|
||||
wf1 = WorkflowBuilder().set_start_executor(c).build()
|
||||
wf1 = WorkflowBuilder(start_executor=c).build()
|
||||
result1: WorkflowRunResult = await wf1.run("done")
|
||||
assert result1.get_final_state() == WorkflowRunState.IDLE
|
||||
|
||||
# Idle-with-pending-request case
|
||||
simple_executor = SimpleExecutor(id="simple")
|
||||
requester = Requester(id="req")
|
||||
wf2 = WorkflowBuilder().set_start_executor(simple_executor).add_edge(simple_executor, requester).build()
|
||||
wf2 = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, requester).build()
|
||||
result2: WorkflowRunResult = await wf2.run("start")
|
||||
assert result2.get_final_state() == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS
|
||||
|
||||
|
||||
async def test_run_includes_status_events_completed():
|
||||
c = Completer(id="c2")
|
||||
wf = WorkflowBuilder().set_start_executor(c).build()
|
||||
wf = WorkflowBuilder(start_executor=c).build()
|
||||
result: WorkflowRunResult = await wf.run("ok")
|
||||
timeline = result.status_timeline()
|
||||
assert timeline, "Expected status timeline in non-streaming run() results"
|
||||
@@ -205,7 +205,7 @@ async def test_run_includes_status_events_completed():
|
||||
async def test_run_includes_status_events_idle_with_requests():
|
||||
simple_executor = SimpleExecutor(id="simple")
|
||||
requester = Requester(id="req2")
|
||||
wf = WorkflowBuilder().set_start_executor(simple_executor).add_edge(simple_executor, requester).build()
|
||||
wf = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, requester).build()
|
||||
result: WorkflowRunResult = await wf.run("start")
|
||||
timeline = result.status_timeline()
|
||||
assert timeline, "Expected status timeline in non-streaming run() results"
|
||||
|
||||
Reference in New Issue
Block a user