Skip to content

Commit 0ebeea5

Browse files
authored
Merge pull request #1490 from Steve-Dusty/fix/graph-workflow-max-loops
fix: make GraphWorkflow max_loops execute all iterations
2 parents 7c311cd + 7f90de5 commit 0ebeea5

2 files changed

Lines changed: 130 additions & 13 deletions

File tree

swarms/structs/graph_workflow.py

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,6 +1582,7 @@ def _build_prompt(
15821582
task: str,
15831583
prev_outputs: Dict[str, Any],
15841584
layer_idx: int,
1585+
loop_idx: int = 0,
15851586
) -> str:
15861587
"""
15871588
Optimized prompt building with minimal string operations.
@@ -1590,14 +1591,17 @@ def _build_prompt(
15901591
node_id (str): The node ID to build a prompt for.
15911592
task (str): The main task.
15921593
prev_outputs (Dict[str, Any]): Previous outputs from predecessor nodes.
1594+
For loop_idx > 0 this also contains end-point outputs from the
1595+
previous loop iteration.
15931596
layer_idx (int): The current layer index.
1597+
loop_idx (int): The current loop iteration (0-based).
15941598
15951599
Returns:
15961600
str: The built prompt.
15971601
"""
15981602
if self.verbose:
15991603
logger.debug(
1600-
f"Building prompt for node {node_id} (layer {layer_idx})"
1604+
f"Building prompt for node {node_id} (layer {layer_idx}, loop {loop_idx})"
16011605
)
16021606

16031607
try:
@@ -1626,6 +1630,24 @@ def _build_prompt(
16261630
f"If you disagree or find gaps, explain why and provide corrections or improvements. "
16271631
f"Your goal is to collaborate and create a comprehensive response that builds on all previous work."
16281632
)
1633+
elif loop_idx > 0 and layer_idx == 0 and prev_outputs:
1634+
# Entry-point nodes in subsequent loops receive end-point
1635+
# outputs from the previous loop as refinement context.
1636+
prior_parts = [
1637+
f"Output from {nid} (previous iteration):\n{out}"
1638+
for nid, out in prev_outputs.items()
1639+
if out is not None
1640+
]
1641+
prior_context = "\n\n".join(prior_parts)
1642+
1643+
prompt = (
1644+
f"Original Task: {task}\n\n"
1645+
f"Previous Iteration Outputs:\n{prior_context}\n\n"
1646+
f"Instructions: This is iteration {loop_idx + 1} of the workflow. "
1647+
f"Review the outputs from the previous iteration above. "
1648+
f"Refine, correct, or expand upon the previous results. "
1649+
f"Focus on improving accuracy, filling gaps, and strengthening the analysis."
1650+
)
16291651
else:
16301652
prompt = (
16311653
f"{task}\n\n"
@@ -1691,14 +1713,22 @@ def run(
16911713
"""
16921714
Run the workflow graph with optimized parallel agent execution.
16931715
1716+
When max_loops > 1, the graph is executed multiple times. End-point
1717+
outputs from each loop are fed as additional context into the next
1718+
loop so that agents can iteratively refine their results.
1719+
16941720
Args:
16951721
task (Optional[str]): Task to execute. Uses self.task if not provided.
16961722
img (Optional[str]): Optional image path for multimodal tasks.
16971723
*args: Additional positional arguments.
16981724
**kwargs: Additional keyword arguments.
16991725
17001726
Returns:
1701-
Dict[str, Any]: Execution results from all nodes.
1727+
Dict[str, Any]: Execution results keyed by node ID.
1728+
When max_loops == 1, returns the single loop's results.
1729+
When max_loops > 1, returns a dict with per-loop results
1730+
keyed as ``{node_id}_loop_{loop_number}`` plus the final
1731+
loop's results under the plain ``node_id`` keys.
17021732
"""
17031733
run_start_time = time.time()
17041734

@@ -1731,6 +1761,11 @@ def run(
17311761

17321762
try:
17331763
loop = 0
1764+
# Accumulated results across all loops
1765+
all_loop_results: Dict[str, Any] = {}
1766+
# End-point outputs carried forward as context for the next loop
1767+
prior_loop_end_outputs: Dict[str, Any] = {}
1768+
17341769
while loop < self.max_loops:
17351770
loop_start_time = time.time()
17361771

@@ -1747,6 +1782,11 @@ def run(
17471782
execution_results = {}
17481783
prev_outputs = {}
17491784

1785+
# Seed entry-point nodes with end-point outputs from the
1786+
# previous loop so agents can refine iteratively.
1787+
if prior_loop_end_outputs:
1788+
prev_outputs.update(prior_loop_end_outputs)
1789+
17501790
for layer_idx, layer in enumerate(
17511791
self._sorted_layers
17521792
):
@@ -1762,7 +1802,7 @@ def run(
17621802
for node_id in layer:
17631803
try:
17641804
prompt = self._build_prompt(
1765-
node_id, task, prev_outputs, layer_idx
1805+
node_id, task, prev_outputs, layer_idx, loop
17661806
)
17671807
layer_data.append(
17681808
(
@@ -1894,20 +1934,40 @@ def run(
18941934
f"Loop {loop}/{self.max_loops} completed in {loop_execution_time:.3f}s"
18951935
)
18961936

1897-
# For now, we still return after the first loop
1898-
# This maintains backward compatibility
1899-
total_execution_time = time.time() - run_start_time
1937+
# Capture end-point outputs to pass as context into the next loop
1938+
prior_loop_end_outputs = {
1939+
node_id: execution_results[node_id]
1940+
for node_id in self.end_points
1941+
if node_id in execution_results
1942+
}
19001943

1901-
logger.info(
1902-
f"GraphWorkflow execution completed: {len(execution_results)} agents executed in {total_execution_time:.3f}s"
1944+
# Accumulate per-loop results (keyed to avoid overwriting)
1945+
if self.max_loops > 1:
1946+
for node_id, output in execution_results.items():
1947+
all_loop_results[
1948+
f"{node_id}_loop_{loop}"
1949+
] = output
1950+
1951+
# Build final return value
1952+
total_execution_time = time.time() - run_start_time
1953+
1954+
logger.info(
1955+
f"GraphWorkflow execution completed: {len(execution_results)} agents executed across {self.max_loops} loop(s) in {total_execution_time:.3f}s"
1956+
)
1957+
1958+
if self.verbose:
1959+
logger.debug(
1960+
f"Final execution results: {list(execution_results.keys())}"
19031961
)
19041962

1905-
if self.verbose:
1906-
logger.debug(
1907-
f"Final execution results: {list(execution_results.keys())}"
1908-
)
1963+
# For single-loop (the common case), return results directly.
1964+
# For multi-loop, merge the per-loop history with the final
1965+
# loop's results so callers can access both.
1966+
if self.max_loops > 1:
1967+
all_loop_results.update(execution_results)
1968+
return all_loop_results
19091969

1910-
return execution_results
1970+
return execution_results
19111971

19121972
except Exception as e:
19131973
total_time = time.time() - run_start_time

tests/structs/test_graph_workflow.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,5 +548,62 @@ def test_graph_workflow_backend_fallback():
548548
)
549549

550550

551+
@pytest.mark.parametrize("backend", ["networkx", "rustworkx"])
552+
def test_graph_workflow_max_loops_accumulates_results(backend):
553+
"""Test that max_loops > 1 actually executes multiple iterations and
554+
accumulates results across loops (fixes #1481)."""
555+
if backend == "rustworkx" and not RUSTWORKX_AVAILABLE:
556+
pytest.skip("rustworkx not available")
557+
558+
agent1 = create_test_agent("Agent1", "Entry agent")
559+
agent2 = create_test_agent("Agent2", "End agent")
560+
561+
workflow = GraphWorkflow(
562+
name=f"MultiLoop-Test-{backend}",
563+
backend=backend,
564+
max_loops=3,
565+
)
566+
workflow.add_node(agent1)
567+
workflow.add_node(agent2)
568+
workflow.add_edge(agent1, agent2)
569+
570+
result = workflow.run("Iteratively refine analysis")
571+
assert result is not None
572+
573+
# With max_loops > 1, result should contain per-loop keys
574+
assert "Agent1_loop_1" in result
575+
assert "Agent2_loop_1" in result
576+
assert "Agent1_loop_2" in result
577+
assert "Agent2_loop_2" in result
578+
assert "Agent1_loop_3" in result
579+
assert "Agent2_loop_3" in result
580+
581+
# Final loop results should also be accessible under plain node IDs
582+
assert "Agent1" in result
583+
assert "Agent2" in result
584+
585+
586+
def test_graph_workflow_single_loop_backward_compatible():
587+
"""Test that max_loops=1 (the default) returns results in the original
588+
format — plain node-ID keys, no loop suffixes."""
589+
agent1 = create_test_agent("Agent1", "Entry agent")
590+
agent2 = create_test_agent("Agent2", "End agent")
591+
592+
workflow = GraphWorkflow(name="SingleLoop-Compat")
593+
workflow.add_node(agent1)
594+
workflow.add_node(agent2)
595+
workflow.add_edge(agent1, agent2)
596+
597+
result = workflow.run("Simple task")
598+
assert result is not None
599+
assert "Agent1" in result
600+
assert "Agent2" in result
601+
602+
# Should NOT have loop-suffixed keys
603+
assert not any(
604+
k.endswith("_loop_1") for k in result
605+
), "Single-loop results should not contain loop-suffixed keys"
606+
607+
551608
if __name__ == "__main__":
552609
pytest.main([__file__, "-v"])

0 commit comments

Comments
 (0)