flow_nodes 0.1.0

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.
@@ -0,0 +1,299 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/flow_nodes"
4
+ require "securerandom"
5
+
6
+ # This example demonstrates:
7
+ # 1. Full lifecycle hooks (prep, exec, post)
8
+ # 2. Symbol-based actions (preferred over strings)
9
+ # 3. State management across nodes
10
+ # 4. Error handling with retries
11
+ # 5. Conditional branching
12
+
13
+ module AdvancedWorkflow
14
+ class OrderValidationNode < FlowNodes::Node
15
+ def initialize
16
+ super(max_retries: 2, wait: 0.5)
17
+ end
18
+
19
+ def prep(state)
20
+ puts "šŸ” [#{Time.now.strftime('%H:%M:%S')}] Starting order validation..."
21
+ state[:validation_start] = Time.now
22
+ state[:order_id] = SecureRandom.hex(6)
23
+
24
+ # Add validation metadata to params
25
+ @params.merge({
26
+ validation_id: SecureRandom.hex(4),
27
+ validated_at: Time.now
28
+ })
29
+ end
30
+
31
+ def exec(order_data)
32
+ puts "šŸ“ [#{Time.now.strftime('%H:%M:%S')}] Validating order #{order_data[:validation_id]}..."
33
+
34
+ # Simulate validation checks
35
+ return :missing_customer if !order_data[:customer_id]
36
+ return :invalid_items if !order_data[:items] || order_data[:items].empty?
37
+ return :invalid_total if !order_data[:total] || order_data[:total] <= 0
38
+
39
+ # Simulate network failure for retry demo
40
+ if rand < 0.3
41
+ raise StandardError, "Validation service timeout"
42
+ end
43
+
44
+ :valid_order
45
+ end
46
+
47
+ def post(state, params, result)
48
+ duration = Time.now - state[:validation_start]
49
+ puts "āœ… [#{Time.now.strftime('%H:%M:%S')}] Validation completed in #{duration.round(3)}s: #{result}"
50
+
51
+ state[:validation_result] = result
52
+ state[:validation_duration] = duration
53
+ end
54
+
55
+ def exec_fallback(params, exception)
56
+ puts "āš ļø [#{Time.now.strftime('%H:%M:%S')}] Validation failed after retries: #{exception.message}"
57
+ :validation_failed
58
+ end
59
+ end
60
+
61
+ class InventoryCheckNode < FlowNodes::Node
62
+ def prep(state)
63
+ puts "šŸ“¦ [#{Time.now.strftime('%H:%M:%S')}] Checking inventory..."
64
+ state[:inventory_check_start] = Time.now
65
+ nil
66
+ end
67
+
68
+ def exec(order_data)
69
+ puts "šŸ” [#{Time.now.strftime('%H:%M:%S')}] Checking #{order_data[:items].length} items..."
70
+
71
+ # Simulate inventory check
72
+ order_data[:items].each do |item|
73
+ puts " - #{item[:name]}: #{item[:quantity]} units"
74
+ end
75
+
76
+ sleep(0.1) # Simulate processing time
77
+
78
+ # Simulate out of stock scenario
79
+ if order_data[:items].any? { |item| item[:quantity] > 10 }
80
+ return :out_of_stock
81
+ end
82
+
83
+ :inventory_available
84
+ end
85
+
86
+ def post(state, params, result)
87
+ duration = Time.now - state[:inventory_check_start]
88
+ puts "āœ… [#{Time.now.strftime('%H:%M:%S')}] Inventory check completed: #{result}"
89
+
90
+ state[:inventory_result] = result
91
+ state[:inventory_duration] = duration
92
+ end
93
+ end
94
+
95
+ class PaymentProcessingNode < FlowNodes::Node
96
+ def prep(state)
97
+ puts "šŸ’³ [#{Time.now.strftime('%H:%M:%S')}] Processing payment..."
98
+ state[:payment_start] = Time.now
99
+
100
+ # Add payment metadata
101
+ @params.merge({
102
+ payment_id: SecureRandom.hex(8),
103
+ processor: "stripe"
104
+ })
105
+ end
106
+
107
+ def exec(order_data)
108
+ puts "šŸ’° [#{Time.now.strftime('%H:%M:%S')}] Processing $#{order_data[:total]} payment..."
109
+ puts "šŸ“Š Payment ID: #{order_data[:payment_id]}"
110
+
111
+ # Simulate payment processing
112
+ sleep(0.2)
113
+
114
+ # Simulate payment failure
115
+ if order_data[:total] > 1000
116
+ return :payment_declined
117
+ end
118
+
119
+ :payment_successful
120
+ end
121
+
122
+ def post(state, params, result)
123
+ duration = Time.now - state[:payment_start]
124
+ puts "āœ… [#{Time.now.strftime('%H:%M:%S')}] Payment processing completed: #{result}"
125
+
126
+ state[:payment_result] = result
127
+ state[:payment_duration] = duration
128
+ end
129
+ end
130
+
131
+ class OrderFulfillmentNode < FlowNodes::Node
132
+ def prep(state)
133
+ puts "🚚 [#{Time.now.strftime('%H:%M:%S')}] Starting order fulfillment..."
134
+ state[:fulfillment_start] = Time.now
135
+
136
+ @params.merge({
137
+ tracking_id: SecureRandom.hex(10),
138
+ estimated_delivery: Time.now + (3 * 24 * 60 * 60) # 3 days
139
+ })
140
+ end
141
+
142
+ def exec(order_data)
143
+ puts "šŸ“¦ [#{Time.now.strftime('%H:%M:%S')}] Fulfilling order..."
144
+ puts "šŸ“Š Tracking ID: #{order_data[:tracking_id]}"
145
+ puts "🚚 Estimated delivery: #{order_data[:estimated_delivery].strftime('%Y-%m-%d')}"
146
+
147
+ # Simulate fulfillment
148
+ sleep(0.1)
149
+
150
+ :order_shipped
151
+ end
152
+
153
+ def post(state, params, result)
154
+ duration = Time.now - state[:fulfillment_start]
155
+ puts "āœ… [#{Time.now.strftime('%H:%M:%S')}] Order fulfilled in #{duration.round(3)}s"
156
+
157
+ state[:fulfillment_result] = result
158
+ state[:fulfillment_duration] = duration
159
+ end
160
+ end
161
+
162
+ class ErrorHandlerNode < FlowNodes::Node
163
+ def prep(state)
164
+ puts "🚨 [#{Time.now.strftime('%H:%M:%S')}] Handling error condition..."
165
+ state[:error_start] = Time.now
166
+ nil
167
+ end
168
+
169
+ def exec(order_data)
170
+ error_type = @params[:error_type] || "unknown_error"
171
+ puts "āŒ [#{Time.now.strftime('%H:%M:%S')}] Error: #{error_type}"
172
+ puts "šŸ“Š Order ID: #{order_data[:validation_id] || 'N/A'}"
173
+
174
+ # Log error for monitoring
175
+ puts "šŸ“‹ Error logged for investigation"
176
+
177
+ :error_handled
178
+ end
179
+
180
+ def post(state, params, result)
181
+ duration = Time.now - state[:error_start]
182
+ puts "šŸ”§ [#{Time.now.strftime('%H:%M:%S')}] Error handling completed in #{duration.round(3)}s"
183
+
184
+ state[:error_handled] = true
185
+ state[:error_duration] = duration
186
+ end
187
+ end
188
+
189
+ class CompletionNode < FlowNodes::Node
190
+ def prep(state)
191
+ puts "šŸŽÆ [#{Time.now.strftime('%H:%M:%S')}] Finalizing order..."
192
+ state[:completion_start] = Time.now
193
+ nil
194
+ end
195
+
196
+ def exec(order_data)
197
+ puts "šŸŽ‰ [#{Time.now.strftime('%H:%M:%S')}] Order completed successfully!"
198
+ puts "šŸ“Š Final order data:"
199
+ puts " - Order ID: #{order_data[:validation_id] || 'N/A'}"
200
+ puts " - Payment ID: #{order_data[:payment_id] || 'N/A'}"
201
+ puts " - Tracking ID: #{order_data[:tracking_id] || 'N/A'}"
202
+ puts " - Total: $#{order_data[:total] || 'N/A'}"
203
+
204
+ nil # End flow
205
+ end
206
+
207
+ def post(state, params, result)
208
+ duration = Time.now - state[:completion_start]
209
+
210
+ puts "āœ… [#{Time.now.strftime('%H:%M:%S')}] Order workflow completed!"
211
+ puts "šŸ“Š Performance metrics:"
212
+ if state[:validation_start]
213
+ total_duration = Time.now - state[:validation_start]
214
+ puts " - Total time: #{total_duration.round(3)}s"
215
+ end
216
+ puts " - Validation: #{state[:validation_duration]&.round(3)}s"
217
+ puts " - Inventory: #{state[:inventory_duration]&.round(3)}s"
218
+ puts " - Payment: #{state[:payment_duration]&.round(3)}s"
219
+ puts " - Fulfillment: #{state[:fulfillment_duration]&.round(3)}s"
220
+ end
221
+ end
222
+ end
223
+
224
+ # Demo script
225
+ if $PROGRAM_NAME == __FILE__
226
+ # Create nodes
227
+ validator = AdvancedWorkflow::OrderValidationNode.new
228
+ inventory = AdvancedWorkflow::InventoryCheckNode.new
229
+ payment = AdvancedWorkflow::PaymentProcessingNode.new
230
+ fulfillment = AdvancedWorkflow::OrderFulfillmentNode.new
231
+ error_handler = AdvancedWorkflow::ErrorHandlerNode.new
232
+ completion = AdvancedWorkflow::CompletionNode.new
233
+
234
+ # Connect workflow with symbol-based actions
235
+ validator - :valid_order >> inventory
236
+ validator - :missing_customer >> error_handler
237
+ validator - :invalid_items >> error_handler
238
+ validator - :invalid_total >> error_handler
239
+ validator - :validation_failed >> error_handler
240
+
241
+ inventory - :inventory_available >> payment
242
+ inventory - :out_of_stock >> error_handler
243
+
244
+ payment - :payment_successful >> fulfillment
245
+ payment - :payment_declined >> error_handler
246
+
247
+ fulfillment - :order_shipped >> completion
248
+
249
+ # Create flow
250
+ flow = FlowNodes::Flow.new(start: validator)
251
+
252
+ # Test scenarios
253
+ puts "=" * 60
254
+ puts "šŸ›’ E-COMMERCE ORDER PROCESSING WORKFLOW"
255
+ puts "=" * 60
256
+
257
+ # Scenario 1: Successful order
258
+ puts "\nšŸ“‹ SCENARIO 1: Successful Order"
259
+ puts "-" * 40
260
+ state = { session_id: SecureRandom.hex(4) }
261
+ flow.set_params({
262
+ customer_id: "cust_123",
263
+ items: [
264
+ { name: "Widget A", quantity: 2, price: 29.99 },
265
+ { name: "Widget B", quantity: 1, price: 19.99 }
266
+ ],
267
+ total: 79.97
268
+ })
269
+ flow.run(state)
270
+
271
+ # Scenario 2: Out of stock
272
+ puts "\nšŸ“‹ SCENARIO 2: Out of Stock"
273
+ puts "-" * 40
274
+ state = { session_id: SecureRandom.hex(4) }
275
+ flow.set_params({
276
+ customer_id: "cust_456",
277
+ items: [
278
+ { name: "Popular Item", quantity: 15, price: 49.99 }
279
+ ],
280
+ total: 49.99
281
+ })
282
+ flow.run(state)
283
+
284
+ # Scenario 3: Invalid order
285
+ puts "\nšŸ“‹ SCENARIO 3: Invalid Order (Missing Customer)"
286
+ puts "-" * 40
287
+ state = { session_id: SecureRandom.hex(4) }
288
+ flow.set_params({
289
+ items: [
290
+ { name: "Widget C", quantity: 1, price: 39.99 }
291
+ ],
292
+ total: 39.99
293
+ })
294
+ flow.run(state)
295
+
296
+ puts "\n" + "=" * 60
297
+ puts "šŸŽÆ WORKFLOW DEMONSTRATIONS COMPLETE"
298
+ puts "=" * 60
299
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/flow_nodes"
4
+
5
+ module BatchDemo
6
+ class DataTransformNode < FlowNodes::BatchNode
7
+ def exec(item)
8
+ puts "Processing item: #{item}"
9
+ # Simulate some processing time
10
+ sleep(0.01)
11
+
12
+ # Transform the data
13
+ {
14
+ original: item,
15
+ processed: item.to_s.upcase,
16
+ timestamp: Time.now
17
+ }
18
+ end
19
+ end
20
+
21
+ class AsyncDataTransformNode < FlowNodes::AsyncBatchNode
22
+ def exec_async(item)
23
+ puts "Async processing item: #{item}"
24
+ # Simulate async processing time
25
+ sleep(0.01)
26
+
27
+ # Transform the data
28
+ {
29
+ original: item,
30
+ processed: item.to_s.upcase,
31
+ timestamp: Time.now,
32
+ thread_id: Thread.current.object_id
33
+ }
34
+ end
35
+ end
36
+
37
+ class ParallelDataTransformNode < FlowNodes::AsyncParallelBatchNode
38
+ def exec_async(item)
39
+ puts "Parallel processing item: #{item} on thread #{Thread.current.object_id}"
40
+ # Simulate processing time
41
+ sleep(0.1)
42
+
43
+ # Transform the data
44
+ {
45
+ original: item,
46
+ processed: item.to_s.upcase,
47
+ timestamp: Time.now,
48
+ thread_id: Thread.current.object_id
49
+ }
50
+ end
51
+ end
52
+
53
+ class BatchCollectorFlow < FlowNodes::BatchFlow
54
+ def prep(state)
55
+ state[:results] = []
56
+ [1, 2, 3, 4, 5] # Return batch items
57
+ end
58
+
59
+ def post(state, prep_result, exec_result)
60
+ puts "Batch processing completed!"
61
+ puts "Results stored in state: #{state[:results].length} items"
62
+ end
63
+ end
64
+
65
+ class ResultCollectorNode < FlowNodes::Node
66
+ def exec(processed_item)
67
+ puts "Collecting result: #{processed_item}"
68
+ nil # End the flow for each item
69
+ end
70
+ end
71
+ end
72
+
73
+ # Demo script
74
+ if $PROGRAM_NAME == __FILE__
75
+ puts "=== Sequential Batch Processing ==="
76
+ transformer = BatchDemo::DataTransformNode.new
77
+ transformer.set_params([1, 2, 3, 4, 5])
78
+ results = transformer.run(nil)
79
+ puts "Sequential results: #{results}"
80
+
81
+ puts "\n=== Async Sequential Batch Processing ==="
82
+ async_transformer = BatchDemo::AsyncDataTransformNode.new
83
+ async_transformer.set_params([1, 2, 3, 4, 5])
84
+ start_time = Time.now
85
+ results = async_transformer.run_async(nil)
86
+ end_time = Time.now
87
+ puts "Async sequential results: #{results}"
88
+ puts "Time taken: #{end_time - start_time} seconds"
89
+
90
+ puts "\n=== Parallel Batch Processing ==="
91
+ parallel_transformer = BatchDemo::ParallelDataTransformNode.new
92
+ parallel_transformer.set_params([1, 2, 3, 4, 5])
93
+ start_time = Time.now
94
+ results = parallel_transformer.run_async(nil)
95
+ end_time = Time.now
96
+ puts "Parallel results: #{results}"
97
+ puts "Time taken: #{end_time - start_time} seconds"
98
+ puts "Notice different thread IDs showing parallel execution"
99
+
100
+ puts "\n=== Batch Flow Example ==="
101
+ transformer = BatchDemo::DataTransformNode.new
102
+ collector = BatchDemo::ResultCollectorNode.new
103
+
104
+ transformer >> collector
105
+
106
+ flow = BatchDemo::BatchCollectorFlow.new(start: transformer)
107
+ flow.run({ results: [] })
108
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/flow_nodes"
4
+ require "securerandom"
5
+
6
+ module ChatDemo
7
+ # Simple utility to simulate an LLM call
8
+ module Utils
9
+ def self.call_llm(messages)
10
+ # In a real implementation, this would call OpenAI, Claude, etc.
11
+ # For demo purposes, we'll just echo the last user message
12
+ last_user_message = messages.reverse.find { |msg| msg[:role] == :user }
13
+ return "I heard you say: #{last_user_message[:content]}" if last_user_message
14
+
15
+ "Hello! How can I help you today?"
16
+ end
17
+ end
18
+
19
+ class ChatNode < FlowNodes::Node
20
+ # Initialize messages array + prompt for user input.
21
+ def prep(shared)
22
+ unless shared.key?(:messages)
23
+ shared[:messages] = []
24
+ shared[:chat_session_id] = SecureRandom.hex(8)
25
+ shared[:start_time] = Time.now
26
+ puts "šŸ¤– Welcome to the chat! Type 'exit' to end the conversation."
27
+ puts "šŸ“Š Session ID: #{shared[:chat_session_id]}"
28
+ end
29
+
30
+ print "\nYou: "
31
+ user_input = $stdin.gets
32
+ return nil unless user_input # EOF (Ctrl-D)
33
+
34
+ user_input = user_input.chomp
35
+ return nil if user_input.strip.downcase == "exit"
36
+
37
+ # push user message
38
+ shared[:messages] << { role: :user, content: user_input }
39
+ shared[:turn_count] = (shared[:turn_count] || 0) + 1
40
+
41
+ puts "šŸ”„ Processing turn #{shared[:turn_count]}..."
42
+ shared[:messages]
43
+ end
44
+
45
+ # Call the LLM with full history.
46
+ def exec(messages)
47
+ return nil unless messages
48
+
49
+ puts "🧠 Calling LLM with #{messages.length} messages..."
50
+ Utils.call_llm(messages)
51
+ end
52
+
53
+ # Print assistant reply, store it, and loop.
54
+ def post(shared, prep_res, exec_res)
55
+ if prep_res.nil? || exec_res.nil?
56
+ duration = Time.now - shared[:start_time]
57
+ puts "\nšŸ‘‹ Goodbye!"
58
+ puts "šŸ“Š Chat session stats:"
59
+ puts " - Session ID: #{shared[:chat_session_id]}"
60
+ puts " - Total turns: #{shared[:turn_count] || 0}"
61
+ puts " - Duration: #{duration.round(2)}s"
62
+ puts " - Messages: #{shared[:messages]&.length || 0}"
63
+ return nil
64
+ end
65
+
66
+ puts "\nšŸ¤– Assistant: #{exec_res}"
67
+ shared[:messages] << { role: :assistant, content: exec_res }
68
+ :continue
69
+ end
70
+
71
+ # Optional: graceful fallback on final retry failure.
72
+ def exec_fallback(_prep_res, exc)
73
+ puts "āš ļø LLM service unavailable: #{exc.class}: #{exc.message}"
74
+ puts "šŸ”„ Retrying in a moment..."
75
+ :continue
76
+ end
77
+ end
78
+ end
79
+
80
+ # Demo script
81
+ if $PROGRAM_NAME == __FILE__
82
+ shared = {}
83
+ chat_node = ChatDemo::ChatNode.new
84
+
85
+ # Create self-loop: when exec returns "continue", go back to same node
86
+ chat_node - :continue >> chat_node
87
+
88
+ # Create flow and run
89
+ flow = FlowNodes::Flow.new(start: chat_node)
90
+ flow.run(shared)
91
+ end