ask-graph 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +145 -116
- data/lib/ask/graph/context.rb +2 -1
- data/lib/ask/graph/version.rb +1 -1
- data/lib/ask/graph.rb +18 -13
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 99d66a3b6b63d97a13187b67bbefd25c291d5ab80a63146e61639a0b7949689d
|
|
4
|
+
data.tar.gz: ab66b1d0586ee8570e71f2612938d6d87a9e3c5a3d44b93b046ef95343a5951a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8cd5ca7104c5ea6f643e9f9333ec8a7578b53f0fcda6c0f8a5a4273ac9cbb856025024e192a9f89c4a30fc3c8072b6b2b46283ccdf3805db05faade627fef81c
|
|
7
|
+
data.tar.gz: f8f4e2494be0350b17b86f568c1de1428a1731b73f9599132f6fe408a70fed15649c803847cf01a36ba009b819f2bcfaf3f37df6d8db2a8dd36dface2c8ae01b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
## [0.6.1] — 2026-07-27
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **Hooks leak between classes** — `inherited` now deep-copies lifecycle
|
|
6
|
+
hook arrays instead of sharing them. Previously, a child class's
|
|
7
|
+
`after_step :foo` would mutate the parent class's hook arrays, causing
|
|
8
|
+
hooks to fire multiple times in sibling classes.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Docs updated** to use the `Module::Workflow` convention throughout.
|
|
13
|
+
Recommended layout:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
app/workflows/
|
|
17
|
+
notify_customer/
|
|
18
|
+
workflow.rb # module NotifyCustomer; class Workflow < Ask::Graph
|
|
19
|
+
steps/
|
|
20
|
+
send_email.rb
|
|
21
|
+
log_notification.rb
|
|
22
|
+
order_fulfillment/
|
|
23
|
+
workflow.rb
|
|
24
|
+
steps/
|
|
25
|
+
validate_payment.rb
|
|
26
|
+
notify_customer.rb
|
|
27
|
+
ship_order.rb
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Sub-graph composition via `Workflow.call(context)` or `context.run(Workflow)`.
|
|
31
|
+
|
|
32
|
+
### Tested
|
|
33
|
+
|
|
34
|
+
- 85 tests, 111 assertions, 0 failures
|
|
35
|
+
|
|
1
36
|
## [0.6.0] — 2026-07-27
|
|
2
37
|
|
|
3
38
|
### Changed
|
data/README.md
CHANGED
|
@@ -6,28 +6,30 @@
|
|
|
6
6
|
gem "ask-graph"
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
## Quick Start
|
|
10
|
-
|
|
11
|
-
```ruby
|
|
12
|
-
require "ask-graph"
|
|
13
|
-
|
|
14
|
-
# Define a workflow
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require "ask-graph"
|
|
13
|
+
|
|
14
|
+
# Define a workflow
|
|
15
|
+
module ProcessOrder
|
|
16
|
+
class Workflow < Ask::Graph
|
|
17
|
+
step ValidateOrder
|
|
18
|
+
step ChargeCustomer, if: :valid?
|
|
19
|
+
step SendConfirmation, if: :valid?
|
|
20
|
+
step NotifyAdmin, unless: :valid?
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def valid?
|
|
25
|
+
context.order.valid?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Run it
|
|
31
|
+
result = ProcessOrder::Workflow.call(order: order)
|
|
32
|
+
```
|
|
31
33
|
|
|
32
34
|
## Steps
|
|
33
35
|
|
|
@@ -41,86 +43,96 @@ class ValidateOrder
|
|
|
41
43
|
end
|
|
42
44
|
```
|
|
43
45
|
|
|
44
|
-
### Sequential steps
|
|
45
|
-
|
|
46
|
-
```ruby
|
|
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
|
-
class
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
end
|
|
123
|
-
|
|
46
|
+
### Sequential steps
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
module HandleCall
|
|
50
|
+
class Workflow < Ask::Graph
|
|
51
|
+
step Transcribe
|
|
52
|
+
step Classify
|
|
53
|
+
step Respond
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Conditional steps
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
module HandleCall
|
|
62
|
+
class Workflow < Ask::Graph
|
|
63
|
+
step BookAppointment, if: :booking?
|
|
64
|
+
step EmergencyAlert, if: :emergency?
|
|
65
|
+
step HandleInquiry, unless: :known_intent?
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def booking? = context.intent == "booking"
|
|
70
|
+
def emergency? = context.intent == "emergency"
|
|
71
|
+
def known_intent? = %w[booking emergency inquiry].include?(context.intent)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Parallel steps
|
|
77
|
+
|
|
78
|
+
Use `steps` (plural) to run multiple steps simultaneously:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
module SyncData
|
|
82
|
+
class Workflow < Ask::Graph
|
|
83
|
+
step FetchRecords
|
|
84
|
+
|
|
85
|
+
# All three run in parallel
|
|
86
|
+
steps CrmUpdate, CalendarSync, SendNotification
|
|
87
|
+
|
|
88
|
+
step ConfirmResponse
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Human-in-the-loop (approve)
|
|
94
|
+
|
|
95
|
+
`approve` runs a step, then pauses the workflow and waits for external input:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
module ProcessBooking
|
|
99
|
+
class Workflow < Ask::Graph
|
|
100
|
+
step BookAppointment
|
|
101
|
+
approve ReviewBooking, if: :expensive?
|
|
102
|
+
step ConfirmBooking
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
After `approve` pauses, resume with:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
graph = ProcessBooking::Workflow.new
|
|
111
|
+
result = graph.run # runs, pauses after ReviewBooking
|
|
112
|
+
result = graph.resume(input: "approved") # resumes, runs ConfirmBooking
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Per-item loops
|
|
116
|
+
|
|
117
|
+
Use `context.each` inside a step for iteration with automatic checkpointing:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
class SendVoiceReminders
|
|
121
|
+
def call(context)
|
|
122
|
+
context.each(context.appointments) do |appt|
|
|
123
|
+
PhoneService.call(appt.number, appt.message)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
module DailyReminders
|
|
129
|
+
class Workflow < Ask::Graph
|
|
130
|
+
step FetchAppointments
|
|
131
|
+
step SendVoiceReminders # loops with per-item checkpointing
|
|
132
|
+
step MarkComplete
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
```
|
|
124
136
|
|
|
125
137
|
If the process crashes mid-loop, it resumes from the last completed item.
|
|
126
138
|
|
|
@@ -146,33 +158,48 @@ end
|
|
|
146
158
|
|
|
147
159
|
### Sub-workflows
|
|
148
160
|
|
|
149
|
-
A step can delegate to another workflow
|
|
161
|
+
A step can delegate to another workflow by calling `Workflow.call(context)`.
|
|
162
|
+
The context is automatically exported, passed to the sub-workflow, and
|
|
163
|
+
results are merged back on completion:
|
|
150
164
|
|
|
151
165
|
```ruby
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
BookingWorkflow.new.call(context) # sub-workflow as a step
|
|
157
|
-
when "emergency"
|
|
158
|
-
EmergencyWorkflow.new.call(context)
|
|
166
|
+
module OrderFulfillment
|
|
167
|
+
class NotifyCustomer
|
|
168
|
+
def call(context)
|
|
169
|
+
NotifyCustomer::Workflow.call(context)
|
|
159
170
|
end
|
|
160
171
|
end
|
|
172
|
+
|
|
173
|
+
class Workflow < Ask::Graph
|
|
174
|
+
step ValidatePayment
|
|
175
|
+
step NotifyCustomer
|
|
176
|
+
step ShipOrder
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Or use `context.run` for the same thing:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
class NotifyCustomer
|
|
185
|
+
def call(context)
|
|
186
|
+
context.run(NotifyCustomer::Workflow)
|
|
187
|
+
end
|
|
161
188
|
end
|
|
162
189
|
```
|
|
163
190
|
|
|
164
191
|
### Checkpointing
|
|
165
192
|
|
|
166
|
-
Pass a
|
|
193
|
+
Pass a `storage` backend to make workflows durable across crashes:
|
|
167
194
|
|
|
168
195
|
```ruby
|
|
169
196
|
store = Ask::State::Memory.new # or Redis, SQLite, etc.
|
|
170
197
|
|
|
171
198
|
# Runs through all steps, saving after each
|
|
172
|
-
result =
|
|
199
|
+
result = OrderFulfillment::Workflow.call(input, storage: store)
|
|
173
200
|
|
|
174
201
|
# If a crash occurs, resume from the last completed step
|
|
175
|
-
result =
|
|
202
|
+
result = OrderFulfillment::Workflow.call(input, storage: store)
|
|
176
203
|
```
|
|
177
204
|
|
|
178
205
|
### Error handling
|
|
@@ -180,12 +207,14 @@ result = MyWorkflow.run(input, checkpoint_store: store)
|
|
|
180
207
|
Failed steps raise `Ask::Graph::StepFailed` with the step name:
|
|
181
208
|
|
|
182
209
|
```ruby
|
|
183
|
-
|
|
184
|
-
|
|
210
|
+
module MyWorkflow
|
|
211
|
+
class Workflow < Ask::Graph
|
|
212
|
+
step RiskyOperation
|
|
213
|
+
end
|
|
185
214
|
end
|
|
186
215
|
|
|
187
216
|
begin
|
|
188
|
-
|
|
217
|
+
MyWorkflow::Workflow.call
|
|
189
218
|
rescue Ask::Graph::StepFailed => e
|
|
190
219
|
puts e.message # => "RiskyOperation failed: connection timeout"
|
|
191
220
|
end
|
data/lib/ask/graph/context.rb
CHANGED
|
@@ -131,9 +131,10 @@ module Ask
|
|
|
131
131
|
# @param graph_class [Class < Ask::Graph] the graph to run
|
|
132
132
|
# @return [Ask::Graph::Context] the sub-graph's context
|
|
133
133
|
# @example
|
|
134
|
+
# # Inside a step PORO
|
|
134
135
|
# class CalculateShipping
|
|
135
136
|
# def call(context)
|
|
136
|
-
# context.run(
|
|
137
|
+
# context.run(Shipping::Workflow)
|
|
137
138
|
# end
|
|
138
139
|
# end
|
|
139
140
|
def run(graph_class)
|
data/lib/ask/graph/version.rb
CHANGED
data/lib/ask/graph.rb
CHANGED
|
@@ -11,28 +11,32 @@ module Ask
|
|
|
11
11
|
# lifecycle hooks.
|
|
12
12
|
#
|
|
13
13
|
# @example Basic workflow
|
|
14
|
-
# class HandleCall < Ask::Graph
|
|
14
|
+
# class HandleCall::Workflow < Ask::Graph
|
|
15
15
|
# step Transcribe
|
|
16
16
|
# step Classify, if: :needs_classification?
|
|
17
17
|
# step BookAppointment
|
|
18
18
|
# end
|
|
19
19
|
#
|
|
20
20
|
# @example Sub-graph composition
|
|
21
|
-
# # Define a workflow
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
21
|
+
# # Define a reusable workflow
|
|
22
|
+
# module NotifyCustomer
|
|
23
|
+
# class Workflow < Ask::Graph
|
|
24
|
+
# step SendEmail
|
|
25
|
+
# step LogNotification
|
|
26
|
+
# end
|
|
25
27
|
# end
|
|
26
28
|
#
|
|
27
29
|
# # Wrap it in a PORO step
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
30
|
+
# module OrderFulfillment
|
|
31
|
+
# class NotifyCustomer
|
|
32
|
+
# def call(context)
|
|
33
|
+
# NotifyCustomer::Workflow.call(context)
|
|
34
|
+
# end
|
|
31
35
|
# end
|
|
32
36
|
# end
|
|
33
37
|
#
|
|
34
38
|
# # Compose — every step is a PORO
|
|
35
|
-
# class
|
|
39
|
+
# class OrderFulfillment::Workflow < Ask::Graph
|
|
36
40
|
# step ValidatePayment
|
|
37
41
|
# step NotifyCustomer
|
|
38
42
|
# step ShipOrder
|
|
@@ -65,7 +69,8 @@ module Ask
|
|
|
65
69
|
def inherited(subclass)
|
|
66
70
|
super
|
|
67
71
|
subclass.instance_variable_set(:@declarations, declarations.dup)
|
|
68
|
-
subclass.instance_variable_set(:@lifecycle_hooks,
|
|
72
|
+
subclass.instance_variable_set(:@lifecycle_hooks,
|
|
73
|
+
lifecycle_hooks.transform_values(&:dup))
|
|
69
74
|
end
|
|
70
75
|
|
|
71
76
|
# --- Timeout ---
|
|
@@ -203,12 +208,12 @@ module Ask
|
|
|
203
208
|
# are merged back into the original context automatically.
|
|
204
209
|
#
|
|
205
210
|
# @example Standalone — pass data
|
|
206
|
-
#
|
|
211
|
+
# OrderFulfillment::Workflow.call({ user: "alice" })
|
|
207
212
|
#
|
|
208
213
|
# @example Inside a step — pass the outer context
|
|
209
|
-
# class
|
|
214
|
+
# class ValidatePayment
|
|
210
215
|
# def call(context)
|
|
211
|
-
#
|
|
216
|
+
# PaymentGateway::Workflow.call(context)
|
|
212
217
|
# end
|
|
213
218
|
# end
|
|
214
219
|
def call(input = nil, storage: nil)
|