-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path02_parallel_and_loop.py
More file actions
194 lines (145 loc) · 5.99 KB
/
Copy path02_parallel_and_loop.py
File metadata and controls
194 lines (145 loc) · 5.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
"""End-to-end demo of the brief §2.5 state graph.
Demonstrates the full agentflow.statemachine MVP:
Research → Parallel(WriteIntro, WriteBody) → Review → (loop | StdEnd)
The graph cycles until Review approves — after APPROVE_AFTER rejections.
FakeLlmConnector is used so no real LLM calls are made.
"""
# Run:
# uv run python examples/framework/02_parallel_and_loop.py -h # help
# uv run python examples/framework/02_parallel_and_loop.py run # run workflow
# uv run python examples/framework/02_parallel_and_loop.py graph --browser
# uv run python examples/framework/02_parallel_and_loop.py graph -o graph.html
import operator
from dataclasses import dataclass
from enum import Enum, auto
from typing import Annotated, Any
from agentflow import AgentApp
from agentflow.logging_config import setup_pretty_logging
from agentflow.statemachine import (
Context,
Parallel,
StateGraph,
StateGraphRunner,
StateVertex,
StdEnd,
StdSignal,
Transition,
)
from agentflow.statemachine.hooks import LoggingHooks
from agentflow.statemachine.testing import FakeLlmConnector
# How many review rejections must occur before the graph terminates.
_APPROVE_AFTER: int = 2
@dataclass(frozen=True)
class DemoState:
"""Immutable state for the §2.5 demo graph.
Attributes:
messages: Accumulated log of vertex activity; uses operator.add reducer
so parallel vertices append rather than overwrite each other.
iteration: Number of completed review cycles (0-based).
"""
messages: Annotated[tuple[str, ...], operator.add] = ()
iteration: int = 0
@dataclass
class DemoPatch:
"""Mutable patch applied to DemoState after each super-step.
Fields default to None to signal "no change" to apply_patches().
Attributes:
messages: Tuple of new messages to append via the reducer.
iteration: New iteration count; None means leave unchanged.
"""
messages: tuple[str, ...] | None = None
iteration: int | None = None
class CustomSignal(Enum):
"""Domain-specific routing signals for the §2.5 demo graph."""
ok = auto()
approved = auto()
rejected = auto()
class Research(StateVertex):
"""Simulates a research phase — always succeeds with CustomSignal.ok."""
async def run(self, state: DemoState, ctx: Context) -> tuple[Any, Any]:
"""Produce a research-completed message and route with ok.
Args:
state: Current DemoState snapshot.
ctx: Shared context (FakeLlmConnector; not called here).
Returns:
Tuple (CustomSignal.ok, DemoPatch with research message).
"""
patch = DemoPatch(messages=(f"Research completed (cycle={state.iteration}).",))
return CustomSignal.ok, patch
class WriteIntro(StateVertex):
"""Simulates writing the introduction section."""
async def run(self, state: DemoState, ctx: Context) -> tuple[Any, Any]:
"""Append an intro-written message and signal done.
Args:
state: Current DemoState snapshot (unused).
ctx: Shared context (unused).
Returns:
Tuple (StdSignal.done, DemoPatch with intro message).
"""
patch = DemoPatch(messages=("Introduction written.",))
return StdSignal.done, patch
class WriteBody(StateVertex):
"""Simulates writing the body content."""
async def run(self, state: DemoState, ctx: Context) -> tuple[Any, Any]:
"""Append a body-written message and signal done.
Args:
state: Current DemoState snapshot (unused).
ctx: Shared context (unused).
Returns:
Tuple (StdSignal.done, DemoPatch with body message).
"""
patch = DemoPatch(messages=("Body written.",))
return StdSignal.done, patch
class Review(StateVertex):
"""Reviews content and approves after _APPROVE_AFTER rejections.
Reads state.iteration: if >= _APPROVE_AFTER, returns approved;
otherwise increments iteration and returns rejected to trigger a loop.
"""
async def run(self, state: DemoState, ctx: Context) -> tuple[Any, Any]:
"""Approve or reject based on how many cycles have occurred.
Args:
state: Current DemoState snapshot; iteration is read to decide.
ctx: Shared context (unused).
Returns:
(CustomSignal.approved, patch) when iteration >= _APPROVE_AFTER;
(CustomSignal.rejected, patch with incremented iteration) otherwise.
"""
if state.iteration >= _APPROVE_AFTER:
patch = DemoPatch(messages=("Review: content approved.",))
return CustomSignal.approved, patch
new_iter = state.iteration + 1
patch = DemoPatch(
messages=(f"Review: rejected — revision {new_iter} requested.",),
iteration=new_iter,
)
return CustomSignal.rejected, patch
_connector = FakeLlmConnector()
_app = AgentApp(
doc=__doc__,
sample_prompts=[
"Write a short essay about the benefits of AI.",
"Summarize the history of machine learning.",
"Explain the BSP execution model in 3 sentences.",
],
context=Context(),
state_graph=StateGraph(
start=Research,
transitions=[
Transition(Research, CustomSignal.ok, Parallel(WriteIntro, WriteBody)),
Transition(WriteIntro, StdSignal.done, Review),
Transition(WriteBody, StdSignal.done, Review),
Transition(Review, CustomSignal.rejected, Research),
Transition(Review, CustomSignal.approved, StdEnd),
],
),
initial_state_factory=lambda _q: DemoState(),
)
_app._extract_result = ( # type: ignore[method-assign]
lambda state: f"Completed {state.iteration + 1} cycles."
)
_app.connector = _connector # backward compat for e2e tests
def BriefExampleApp() -> AgentApp:
"""Backward-compatible factory returning the module-level app instance."""
return _app
if __name__ == "__main__":
_app.cli(__doc__, name=__name__)