ask-graph 0.1.0 → 0.1.2
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 +38 -0
- data/lib/ask/graph/context.rb +26 -6
- data/lib/ask/graph/runner.rb +32 -12
- data/lib/ask/graph/version.rb +1 -1
- data/lib/ask/graph.rb +21 -41
- 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: c00714c8b8feb0d71aa93636f39f077b14f19ef5405efdc667317f4fe99cc6a2
|
|
4
|
+
data.tar.gz: 29cc8acc5187762918a2084bd2f3578df5ee5e6383a3b6d1edde1e4df519604a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7291024a6f4b113e87f0cf2b6005e6dd44aa3e913dc509350e6d15fb061d64b091d05c7cd6c72dca4dae291a7a329c5d95dda94c80122b128773fda1ae510102
|
|
7
|
+
data.tar.gz: 1d5692fdd06f6a3a626f704f32bef5f75e70078846567650fb583325b188f16f2647ac17e87c0bc3c441f71ac6d1fd8ffce580fddf8f3fa7b999f462b2227079
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
## [0.1.2] — 2026-07-28
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **`Graph.new(input).call` API** — input is now passed to the constructor, not to `call`. This reads more naturally: create a graph with data, then execute it. `Graph.call(input, checkpoint_store:)` class method still available for convenience.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Before
|
|
9
|
+
HandleCall.new.call(recording: "call.wav")
|
|
10
|
+
|
|
11
|
+
# After
|
|
12
|
+
HandleCall.new({ recording: "call.wav" }).call
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Tested
|
|
16
|
+
|
|
17
|
+
- 36 tests, 52 assertions, 0 failures
|
|
18
|
+
|
|
19
|
+
## [0.1.1] — 2026-07-28
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Full context checkpointing** — Checkpoints now save the complete context state alongside the step index. On resume, all data produced by completed steps is restored. Previously only the step index was persisted, causing nil errors when resumed steps referenced prior outputs.
|
|
24
|
+
|
|
25
|
+
- **`call` as primary API** — `Graph.new.call(input)` is now the primary interface, consistent with step classes. `Graph.call` and `Graph.run` remain as convenience class methods.
|
|
26
|
+
|
|
27
|
+
- **Default in-memory store** — Checkpoint store defaults to `Ask::State::Memory` (shipped with `ask-core`, zero additional dependencies). No database, migration, or configuration needed. Pass a persistent store for crash recovery across restarts.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- `context.to_h` now produces JSON-safe hashes (string keys, symbols converted to strings, arrays recursed). Checkpoints serialize correctly in Ruby 4.0.
|
|
32
|
+
|
|
33
|
+
- `initialize(checkpoint_store:)` stores the checkpoint store on the instance. Pass `nil` to use the default in-memory store.
|
|
34
|
+
|
|
35
|
+
### Tested
|
|
36
|
+
|
|
37
|
+
- 36 tests, 52 assertions, 0 failures
|
|
38
|
+
|
|
1
39
|
## [0.1.0] — 2026-07-28
|
|
2
40
|
|
|
3
41
|
### Added
|
data/lib/ask/graph/context.rb
CHANGED
|
@@ -30,9 +30,13 @@ module Ask
|
|
|
30
30
|
@each_items = nil
|
|
31
31
|
@item = nil
|
|
32
32
|
@mutex = Mutex.new
|
|
33
|
+
@resume_input = nil
|
|
33
34
|
@store[:input] = input if input
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
# @return [Object, nil] input provided on resume for approval steps
|
|
38
|
+
attr_accessor :resume_input
|
|
39
|
+
|
|
36
40
|
# Access a stored value by method name.
|
|
37
41
|
def method_missing(name, *args, &block)
|
|
38
42
|
if name.to_s.end_with?("=")
|
|
@@ -89,14 +93,30 @@ module Ask
|
|
|
89
93
|
@item = nil
|
|
90
94
|
end
|
|
91
95
|
|
|
92
|
-
# @return [Hash] serializable snapshot of the context state
|
|
96
|
+
# @return [Hash] serializable snapshot of the context state.
|
|
97
|
+
# All keys and values are converted to JSON-safe types (string keys,
|
|
98
|
+
# symbols converted to strings, arrays recursed).
|
|
93
99
|
def to_h
|
|
94
100
|
@mutex.synchronize do
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
deep_json_safe(@store)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
# Recursively convert a value to a JSON-safe structure.
|
|
108
|
+
def deep_json_safe(value)
|
|
109
|
+
case value
|
|
110
|
+
when Hash
|
|
111
|
+
value.each_with_object({}) { |(k, v), h| h[k.to_s] = deep_json_safe(v) }
|
|
112
|
+
when Array
|
|
113
|
+
value.map { |v| deep_json_safe(v) }
|
|
114
|
+
when Symbol
|
|
115
|
+
value.to_s
|
|
116
|
+
when String, Numeric, true, false, nil
|
|
117
|
+
value
|
|
118
|
+
else
|
|
119
|
+
value.respond_to?(:to_h) ? deep_json_safe(value.to_h) : value.to_s
|
|
100
120
|
end
|
|
101
121
|
end
|
|
102
122
|
end
|
data/lib/ask/graph/runner.rb
CHANGED
|
@@ -12,12 +12,21 @@ module Ask
|
|
|
12
12
|
|
|
13
13
|
# Executes the declared steps of a graph, managing checkpointing,
|
|
14
14
|
# condition evaluation, parallel execution, and human-in-the-loop pauses.
|
|
15
|
+
#
|
|
16
|
+
# Checkpoints save the full context state after each step so that
|
|
17
|
+
# crashes resume from the last completed step with all data intact.
|
|
18
|
+
# Uses an in-memory store by default; pass a persistent store for
|
|
19
|
+
# crash recovery across restarts.
|
|
15
20
|
class Runner
|
|
16
21
|
# @return [Array<Hash>] step declarations with name, type, condition
|
|
17
22
|
attr_reader :declarations
|
|
18
23
|
|
|
24
|
+
# @return [#set, #get, #delete] the checkpoint store
|
|
25
|
+
attr_reader :store
|
|
26
|
+
|
|
19
27
|
# @param declarations [Array<Hash>] step declarations
|
|
20
|
-
# @param checkpoint_store [#set, #get, #delete]
|
|
28
|
+
# @param checkpoint_store [#set, #get, #delete] key-value store.
|
|
29
|
+
# Defaults to in-memory (no durability across restarts).
|
|
21
30
|
def initialize(declarations, checkpoint_store: nil)
|
|
22
31
|
@declarations = declarations
|
|
23
32
|
@store = checkpoint_store
|
|
@@ -28,7 +37,7 @@ module Ask
|
|
|
28
37
|
# @param context [Ask::Graph::Context] the shared context
|
|
29
38
|
# @return [Ask::Graph::Context] the completed context
|
|
30
39
|
def run(context)
|
|
31
|
-
resume_index = load_checkpoint
|
|
40
|
+
resume_index = load_checkpoint(context)
|
|
32
41
|
|
|
33
42
|
@declarations.each_with_index do |decl, idx|
|
|
34
43
|
next if resume_index && idx <= resume_index
|
|
@@ -40,7 +49,7 @@ module Ask
|
|
|
40
49
|
|
|
41
50
|
execute_step(decl, context)
|
|
42
51
|
record_completion(idx, :completed)
|
|
43
|
-
save_checkpoint(idx)
|
|
52
|
+
save_checkpoint(context, idx)
|
|
44
53
|
end
|
|
45
54
|
|
|
46
55
|
context
|
|
@@ -60,7 +69,7 @@ module Ask
|
|
|
60
69
|
# @return [Integer, nil] the index to resume from, or nil
|
|
61
70
|
def resume_index_for(items)
|
|
62
71
|
key = each_checkpoint_key
|
|
63
|
-
raw = @store
|
|
72
|
+
raw = @store.get(key)
|
|
64
73
|
raw ? raw.to_i : nil
|
|
65
74
|
end
|
|
66
75
|
|
|
@@ -68,7 +77,7 @@ module Ask
|
|
|
68
77
|
# @param index [Integer] the completed iteration index
|
|
69
78
|
def checkpoint_each!(index)
|
|
70
79
|
key = each_checkpoint_key
|
|
71
|
-
@store
|
|
80
|
+
@store.set(key, index.to_s)
|
|
72
81
|
end
|
|
73
82
|
|
|
74
83
|
private
|
|
@@ -110,8 +119,7 @@ module Ask
|
|
|
110
119
|
def run_approve(klass, _name, context)
|
|
111
120
|
instance = klass.new
|
|
112
121
|
instance.call(context)
|
|
113
|
-
|
|
114
|
-
save_checkpoint(current_index)
|
|
122
|
+
save_checkpoint(context, current_index)
|
|
115
123
|
raise Paused, "Graph paused for approval after #{klass.name}"
|
|
116
124
|
end
|
|
117
125
|
|
|
@@ -141,16 +149,28 @@ module Ask
|
|
|
141
149
|
"#{checkpoint_key}:each"
|
|
142
150
|
end
|
|
143
151
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
# Save checkpoint with full context state so resumed graphs
|
|
153
|
+
# have access to all data produced by completed steps.
|
|
154
|
+
def save_checkpoint(context, idx)
|
|
155
|
+
data = {
|
|
156
|
+
completed_index: idx,
|
|
157
|
+
context: context.to_h,
|
|
158
|
+
timestamp: Time.now.iso8601
|
|
159
|
+
}
|
|
160
|
+
@store.set(checkpoint_key, JSON.generate(data))
|
|
147
161
|
end
|
|
148
162
|
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
# Load checkpoint and restore context state if available.
|
|
164
|
+
# Returns the completed_index to skip, or nil for a fresh run.
|
|
165
|
+
def load_checkpoint(context)
|
|
166
|
+
raw = @store.get(checkpoint_key)
|
|
151
167
|
return nil unless raw
|
|
152
168
|
|
|
153
169
|
data = JSON.parse(raw)
|
|
170
|
+
restored = data["context"]
|
|
171
|
+
if restored.is_a?(Hash)
|
|
172
|
+
restored.each { |k, v| context[k.to_sym] = v }
|
|
173
|
+
end
|
|
154
174
|
data["completed_index"]
|
|
155
175
|
end
|
|
156
176
|
|
data/lib/ask/graph/version.rb
CHANGED
data/lib/ask/graph.rb
CHANGED
|
@@ -16,28 +16,11 @@ module Ask
|
|
|
16
16
|
# step BookAppointment
|
|
17
17
|
# end
|
|
18
18
|
#
|
|
19
|
-
#
|
|
20
|
-
# class SyncData < Ask::Graph
|
|
21
|
-
# step FetchRecords
|
|
19
|
+
# result = HandleCall.new.call(recording: "call.wav")
|
|
22
20
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# end
|
|
27
|
-
#
|
|
28
|
-
# @example With human-in-the-loop
|
|
29
|
-
# class ProcessBooking < Ask::Graph
|
|
30
|
-
# step BookAppointment
|
|
31
|
-
# approve ReviewBooking, if: :expensive?
|
|
32
|
-
# step ConfirmBooking
|
|
33
|
-
# end
|
|
34
|
-
#
|
|
35
|
-
# @example With per-item looping
|
|
36
|
-
# class SendReminders < Ask::Graph
|
|
37
|
-
# step FetchAppointments
|
|
38
|
-
# step RemindEach # calls context.each(:appointments) internally
|
|
39
|
-
# step MarkComplete
|
|
40
|
-
# end
|
|
21
|
+
# @example With checkpointing
|
|
22
|
+
# store = Ask::State::Memory.new
|
|
23
|
+
# result = HandleCall.new.call(input, checkpoint_store: store)
|
|
41
24
|
#
|
|
42
25
|
class Graph
|
|
43
26
|
class << self
|
|
@@ -95,26 +78,23 @@ module Ask
|
|
|
95
78
|
}
|
|
96
79
|
end
|
|
97
80
|
|
|
98
|
-
#
|
|
99
|
-
# @param input [Object] optional
|
|
81
|
+
# Convenience: create instance and call.
|
|
82
|
+
# @param input [Object] optional input
|
|
100
83
|
# @param checkpoint_store [#set, #get, #delete] optional persistent store
|
|
101
84
|
# @return [Ask::Graph::Context] the completed context
|
|
102
|
-
def
|
|
103
|
-
new(checkpoint_store: checkpoint_store).
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Resume a paused graph with human input.
|
|
107
|
-
# @param input [Object] the human's input
|
|
108
|
-
# @param checkpoint_store [#set, #get, #delete] the same store used for the original run
|
|
109
|
-
# @return [Ask::Graph::Context] the completed context
|
|
110
|
-
def resume(input:, checkpoint_store:)
|
|
111
|
-
new(checkpoint_store: checkpoint_store).resume(input: input)
|
|
85
|
+
def call(input = nil, checkpoint_store: nil)
|
|
86
|
+
new(input, checkpoint_store: checkpoint_store).call
|
|
112
87
|
end
|
|
88
|
+
alias run call
|
|
113
89
|
end
|
|
114
90
|
|
|
115
|
-
# @param
|
|
116
|
-
|
|
117
|
-
|
|
91
|
+
# @param input [Object, nil] initial input for the graph execution
|
|
92
|
+
# @param checkpoint_store [#set, #get, #delete, nil] optional persistent store.
|
|
93
|
+
# Defaults to in-memory store. Pass a persistent store for crash recovery.
|
|
94
|
+
def initialize(input = nil, checkpoint_store: nil)
|
|
95
|
+
@input = input
|
|
96
|
+
store = checkpoint_store || Ask::State::Memory.new
|
|
97
|
+
@runner = Runner.new(self.class.declarations, checkpoint_store: store)
|
|
118
98
|
@context = nil
|
|
119
99
|
end
|
|
120
100
|
|
|
@@ -124,17 +104,17 @@ module Ask
|
|
|
124
104
|
# @return [Ask::Graph::Context, nil] the shared context
|
|
125
105
|
attr_reader :context
|
|
126
106
|
|
|
127
|
-
#
|
|
128
|
-
# @param input [Object] optional initial input
|
|
107
|
+
# Execute the graph.
|
|
129
108
|
# @return [Ask::Graph::Context] the completed context
|
|
130
|
-
def
|
|
131
|
-
@context = Context.new(self, input)
|
|
109
|
+
def call
|
|
110
|
+
@context = Context.new(self, @input)
|
|
132
111
|
@runner.run(@context)
|
|
133
112
|
@context
|
|
134
113
|
rescue => e
|
|
135
|
-
raise unless e.
|
|
114
|
+
raise unless e.is_a?(Paused)
|
|
136
115
|
@context
|
|
137
116
|
end
|
|
117
|
+
alias run call
|
|
138
118
|
|
|
139
119
|
# Resume a paused graph with human input.
|
|
140
120
|
# @param input [Object] the human's input
|