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.
- checksums.yaml +7 -0
- data/.qlty.yml +40 -0
- data/.rspec +3 -0
- data/.rubocop.yml +53 -0
- data/CHANGELOG.md +59 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +315 -0
- data/Rakefile +12 -0
- data/examples/advanced_workflow.rb +299 -0
- data/examples/batch_processing.rb +108 -0
- data/examples/chatbot.rb +91 -0
- data/examples/llm_calendar_parser.rb +429 -0
- data/examples/llm_content_processor.rb +603 -0
- data/examples/llm_document_analyzer.rb +276 -0
- data/examples/simple_llm_example.rb +166 -0
- data/examples/workflow.rb +158 -0
- data/lib/flow_nodes/async_batch_flow.rb +16 -0
- data/lib/flow_nodes/async_batch_node.rb +15 -0
- data/lib/flow_nodes/async_flow.rb +49 -0
- data/lib/flow_nodes/async_node.rb +48 -0
- data/lib/flow_nodes/async_parallel_batch_flow.rb +17 -0
- data/lib/flow_nodes/async_parallel_batch_node.rb +18 -0
- data/lib/flow_nodes/base_node.rb +117 -0
- data/lib/flow_nodes/batch_flow.rb +16 -0
- data/lib/flow_nodes/batch_node.rb +15 -0
- data/lib/flow_nodes/conditional_transition.rb +17 -0
- data/lib/flow_nodes/flow.rb +65 -0
- data/lib/flow_nodes/node.rb +54 -0
- data/lib/flow_nodes/version.rb +5 -0
- data/lib/flow_nodes.rb +20 -0
- data/sig/flow_nodes.rbs +4 -0
- metadata +82 -0
|
@@ -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
|
data/examples/chatbot.rb
ADDED
|
@@ -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
|