langgraph_rb 0.1.0 ā 0.1.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/.gitignore +10 -0
- data/README.md +1 -1
- data/examples/initial_state_example.rb +646 -0
- data/examples/observer_example.rb +121 -0
- data/examples/reducers_example.rb +337 -0
- data/lib/langgraph_rb/graph.rb +6 -4
- data/lib/langgraph_rb/observers/base.rb +188 -0
- data/lib/langgraph_rb/observers/logger.rb +71 -0
- data/lib/langgraph_rb/observers/structured.rb +111 -0
- data/lib/langgraph_rb/runner.rb +106 -3
- data/lib/langgraph_rb/version.rb +1 -1
- data/lib/langgraph_rb.rb +4 -0
- data/test_runner.rb +3 -3
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9c2d260c430ce9be20fea65d6dff4d8013a535a6db46cdd89e15b19200c8b6dd
|
4
|
+
data.tar.gz: a5213444bbf90954158836c6e73237752ecc2d689e0e4de4807621edb2460f44
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce7b663fb858e5c2adea131d21682df32eddc0cbb72b324773fa0b1cf932338c105d155e7b7b046556748e9f5d60342b9171537a21e1f41c7f7e4933499968cb
|
7
|
+
data.tar.gz: e30e5ad223be3a455361f753b44ba928054eff285429ca0a8c73474eb851eb9c6c59c627357010032d5133d8ba9d891669225e3640aea5873017910724766536
|
data/.gitignore
ADDED
data/README.md
CHANGED
@@ -347,4 +347,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
347
347
|
- [ ] Performance optimizations
|
348
348
|
- [ ] More comprehensive test suite
|
349
349
|
- [ ] Integration with Sidekiq for background processing
|
350
|
-
- [
|
350
|
+
- [x] Metrics and observability features
|
@@ -0,0 +1,646 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/langgraph_rb'
|
4
|
+
|
5
|
+
# Custom state class for advanced initial state management
|
6
|
+
class TaskManagerState < LangGraphRB::State
|
7
|
+
def initialize(schema = {}, reducers = nil)
|
8
|
+
# Define default reducers for this state class
|
9
|
+
reducers ||= {
|
10
|
+
tasks: LangGraphRB::State.add_messages, # Tasks will be accumulated
|
11
|
+
history: LangGraphRB::State.add_messages, # History will be accumulated
|
12
|
+
metrics: LangGraphRB::State.merge_hash, # Metrics will be merged
|
13
|
+
tags: ->(old, new) { (old || []) | (new || []) } # Custom reducer: union of arrays
|
14
|
+
}
|
15
|
+
super(schema, reducers)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Example 1: Basic Initial State - Simple Hash
|
20
|
+
def example_1_basic_initial_state
|
21
|
+
puts "\n" + "=" * 70
|
22
|
+
puts "š EXAMPLE 1: Basic Initial State with Simple Hash"
|
23
|
+
puts "=" * 70
|
24
|
+
|
25
|
+
# Create a simple graph that processes user requests
|
26
|
+
graph = LangGraphRB::Graph.new do
|
27
|
+
node :validate_input do |state|
|
28
|
+
input = state[:user_input]
|
29
|
+
|
30
|
+
puts "š Validating input: '#{input}'"
|
31
|
+
|
32
|
+
if input.nil? || input.strip.empty?
|
33
|
+
{
|
34
|
+
valid: false,
|
35
|
+
error: "Input cannot be empty",
|
36
|
+
validation_time: Time.now
|
37
|
+
}
|
38
|
+
else
|
39
|
+
{
|
40
|
+
valid: true,
|
41
|
+
processed_input: input.strip.downcase,
|
42
|
+
validation_time: Time.now
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
node :process_request do |state|
|
48
|
+
unless state[:valid]
|
49
|
+
{ error: "Cannot process invalid input" }
|
50
|
+
else
|
51
|
+
input = state[:processed_input]
|
52
|
+
puts "āļø Processing: #{input}"
|
53
|
+
|
54
|
+
{
|
55
|
+
result: "Processed: #{input}",
|
56
|
+
processing_time: Time.now,
|
57
|
+
word_count: input.split.length
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
node :generate_response do |state|
|
63
|
+
if state[:error]
|
64
|
+
{ response: "Error: #{state[:error]}" }
|
65
|
+
else
|
66
|
+
response = "ā
#{state[:result]} (#{state[:word_count]} words)"
|
67
|
+
puts "š¤ Generated response: #{response}"
|
68
|
+
|
69
|
+
{
|
70
|
+
response: response,
|
71
|
+
completed_at: Time.now,
|
72
|
+
success: true
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
set_entry_point :validate_input
|
78
|
+
edge :validate_input, :process_request
|
79
|
+
edge :process_request, :generate_response
|
80
|
+
set_finish_point :generate_response
|
81
|
+
end
|
82
|
+
|
83
|
+
graph.compile!
|
84
|
+
|
85
|
+
# Test with different initial states
|
86
|
+
test_cases = [
|
87
|
+
{ user_input: "Hello World" }, # Valid input
|
88
|
+
{ user_input: "" }, # Empty input
|
89
|
+
{ user_input: " Process This Request " }, # Input with whitespace
|
90
|
+
{ user_input: nil } # Nil input
|
91
|
+
]
|
92
|
+
|
93
|
+
test_cases.each_with_index do |initial_state, i|
|
94
|
+
puts "\n--- Test Case #{i + 1}: #{initial_state.inspect} ---"
|
95
|
+
|
96
|
+
result = graph.invoke(initial_state)
|
97
|
+
|
98
|
+
puts "Final state keys: #{result.keys.sort}"
|
99
|
+
puts "Success: #{result[:success] || false}"
|
100
|
+
puts "Response: #{result[:response]}" if result[:response]
|
101
|
+
puts "Error: #{result[:error]}" if result[:error]
|
102
|
+
end
|
103
|
+
|
104
|
+
puts "\nā
Example 1 completed!"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Example 2: Initial State with Custom Reducers
|
108
|
+
def example_2_custom_reducers
|
109
|
+
puts "\n" + "=" * 70
|
110
|
+
puts "š EXAMPLE 2: Initial State with Custom Reducers"
|
111
|
+
puts "=" * 70
|
112
|
+
|
113
|
+
# Create a graph that accumulates data using reducers
|
114
|
+
graph = LangGraphRB::Graph.new(state_class: LangGraphRB::State) do
|
115
|
+
node :collect_user_info do |state|
|
116
|
+
puts "š¤ Collecting user info from: #{state[:source]}"
|
117
|
+
|
118
|
+
case state[:source]
|
119
|
+
when 'profile'
|
120
|
+
{
|
121
|
+
user_data: { name: state[:name], email: state[:email] },
|
122
|
+
sources: ['profile'],
|
123
|
+
collection_count: 1
|
124
|
+
}
|
125
|
+
when 'preferences'
|
126
|
+
{
|
127
|
+
user_data: { theme: 'dark', language: 'en' },
|
128
|
+
sources: ['preferences'],
|
129
|
+
collection_count: 1
|
130
|
+
}
|
131
|
+
when 'activity'
|
132
|
+
{
|
133
|
+
user_data: { last_login: Time.now, login_count: 42 },
|
134
|
+
sources: ['activity'],
|
135
|
+
collection_count: 1
|
136
|
+
}
|
137
|
+
else
|
138
|
+
{ sources: ['unknown'], collection_count: 1 }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
node :aggregate_data do |state|
|
143
|
+
puts "š Aggregating collected data"
|
144
|
+
|
145
|
+
{
|
146
|
+
aggregated: true,
|
147
|
+
total_sources: (state[:sources] || []).length,
|
148
|
+
final_user_data: state[:user_data] || {},
|
149
|
+
processing_completed_at: Time.now
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
set_entry_point :collect_user_info
|
154
|
+
edge :collect_user_info, :aggregate_data
|
155
|
+
set_finish_point :aggregate_data
|
156
|
+
end
|
157
|
+
|
158
|
+
graph.compile!
|
159
|
+
|
160
|
+
# Initial state with reducers defined
|
161
|
+
initial_state_with_reducers = LangGraphRB::State.new(
|
162
|
+
{
|
163
|
+
sources: [],
|
164
|
+
user_data: {},
|
165
|
+
collection_count: 0
|
166
|
+
},
|
167
|
+
{
|
168
|
+
sources: ->(old, new) { (old || []) + (new || []) }, # Accumulate sources
|
169
|
+
user_data: LangGraphRB::State.merge_hash, # Merge user data
|
170
|
+
collection_count: ->(old, new) { (old || 0) + (new || 0) } # Sum counts
|
171
|
+
}
|
172
|
+
)
|
173
|
+
|
174
|
+
# Test multiple data collection scenarios
|
175
|
+
scenarios = [
|
176
|
+
{ source: 'profile', name: 'John Doe', email: 'john@example.com' },
|
177
|
+
{ source: 'preferences' },
|
178
|
+
{ source: 'activity' }
|
179
|
+
]
|
180
|
+
|
181
|
+
scenarios.each_with_index do |scenario, i|
|
182
|
+
puts "\n--- Scenario #{i + 1}: #{scenario[:source]} data ---"
|
183
|
+
|
184
|
+
# Create a copy of the initial state for this scenario
|
185
|
+
test_state = initial_state_with_reducers.merge_delta(scenario)
|
186
|
+
|
187
|
+
result = graph.invoke(test_state)
|
188
|
+
|
189
|
+
puts "Sources collected: #{result[:sources]}"
|
190
|
+
puts "Total sources: #{result[:total_sources]}"
|
191
|
+
puts "Collection count: #{result[:collection_count]}"
|
192
|
+
puts "User data: #{result[:final_user_data]}"
|
193
|
+
end
|
194
|
+
|
195
|
+
puts "\nā
Example 2 completed!"
|
196
|
+
end
|
197
|
+
|
198
|
+
# Example 3: Advanced Initial State with Custom State Class
|
199
|
+
def example_3_custom_state_class
|
200
|
+
puts "\n" + "=" * 70
|
201
|
+
puts "šļø EXAMPLE 3: Advanced Initial State with Custom State Class"
|
202
|
+
puts "=" * 70
|
203
|
+
|
204
|
+
# Create a task management graph using custom state class
|
205
|
+
graph = LangGraphRB::Graph.new(state_class: TaskManagerState) do
|
206
|
+
node :add_task do |state|
|
207
|
+
task_title = state[:new_task]
|
208
|
+
unless task_title
|
209
|
+
{ error: "No task provided" }
|
210
|
+
else
|
211
|
+
puts "ā Adding task: #{task_title}"
|
212
|
+
|
213
|
+
new_task = {
|
214
|
+
id: SecureRandom.hex(4),
|
215
|
+
title: task_title,
|
216
|
+
created_at: Time.now,
|
217
|
+
status: 'pending'
|
218
|
+
}
|
219
|
+
|
220
|
+
{
|
221
|
+
tasks: [new_task], # Will be accumulated due to reducer
|
222
|
+
history: ["Task '#{task_title}' added"],
|
223
|
+
metrics: { total_added: 1, last_added_at: Time.now },
|
224
|
+
tags: state[:task_tags] || []
|
225
|
+
}
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
node :update_metrics do |state|
|
230
|
+
task_count = (state[:tasks] || []).length
|
231
|
+
|
232
|
+
puts "š Updating metrics: #{task_count} total tasks"
|
233
|
+
|
234
|
+
{
|
235
|
+
metrics: {
|
236
|
+
task_count: task_count,
|
237
|
+
updated_at: Time.now,
|
238
|
+
active_tasks: (state[:tasks] || []).count { |t| t[:status] == 'pending' }
|
239
|
+
},
|
240
|
+
history: ["Metrics updated: #{task_count} tasks total"]
|
241
|
+
}
|
242
|
+
end
|
243
|
+
|
244
|
+
node :generate_summary do |state|
|
245
|
+
tasks = state[:tasks] || []
|
246
|
+
history = state[:history] || []
|
247
|
+
metrics = state[:metrics] || {}
|
248
|
+
tags = state[:tags] || []
|
249
|
+
|
250
|
+
puts "š Generating summary"
|
251
|
+
|
252
|
+
summary = {
|
253
|
+
total_tasks: tasks.length,
|
254
|
+
total_history_events: history.length,
|
255
|
+
unique_tags: tags.uniq.length,
|
256
|
+
latest_metric_update: metrics[:updated_at],
|
257
|
+
task_titles: tasks.map { |t| t[:title] }
|
258
|
+
}
|
259
|
+
|
260
|
+
{
|
261
|
+
summary: summary,
|
262
|
+
history: ["Summary generated with #{tasks.length} tasks"]
|
263
|
+
}
|
264
|
+
end
|
265
|
+
|
266
|
+
set_entry_point :add_task
|
267
|
+
edge :add_task, :update_metrics
|
268
|
+
edge :update_metrics, :generate_summary
|
269
|
+
set_finish_point :generate_summary
|
270
|
+
end
|
271
|
+
|
272
|
+
graph.compile!
|
273
|
+
|
274
|
+
# Test with rich initial state
|
275
|
+
rich_initial_state = {
|
276
|
+
# Existing data that will be preserved and extended
|
277
|
+
tasks: [
|
278
|
+
{ id: 'existing-1', title: 'Review code', status: 'completed', created_at: Time.now - 3600 }
|
279
|
+
],
|
280
|
+
history: ['System initialized', 'Loaded existing tasks'],
|
281
|
+
metrics: { system_start_time: Time.now - 7200 },
|
282
|
+
tags: ['work', 'priority'],
|
283
|
+
|
284
|
+
# New data for current operation
|
285
|
+
new_task: 'Write documentation',
|
286
|
+
task_tags: ['documentation', 'writing']
|
287
|
+
}
|
288
|
+
|
289
|
+
puts "Initial state structure:"
|
290
|
+
puts " Existing tasks: 1"
|
291
|
+
puts " History events: 2"
|
292
|
+
puts " Existing tags: #{rich_initial_state[:tags]}"
|
293
|
+
puts " New task: #{rich_initial_state[:new_task]}"
|
294
|
+
puts " New tags: #{rich_initial_state[:task_tags]}"
|
295
|
+
|
296
|
+
result = graph.invoke(rich_initial_state)
|
297
|
+
|
298
|
+
puts "\nš Final Results:"
|
299
|
+
puts "Total tasks: #{result[:summary][:total_tasks]}"
|
300
|
+
puts "Task titles: #{result[:summary][:task_titles]}"
|
301
|
+
puts "History events: #{result[:summary][:total_history_events]}"
|
302
|
+
puts "Unique tags: #{result[:summary][:unique_tags]}"
|
303
|
+
puts "Final tags: #{result[:tags]&.uniq}"
|
304
|
+
|
305
|
+
puts "\nš Detailed final state:"
|
306
|
+
result[:tasks]&.each_with_index do |task, i|
|
307
|
+
puts " Task #{i+1}: #{task[:title]} (#{task[:status]})"
|
308
|
+
end
|
309
|
+
|
310
|
+
puts "\nš History:"
|
311
|
+
result[:history]&.each_with_index do |event, i|
|
312
|
+
puts " #{i+1}. #{event}"
|
313
|
+
end
|
314
|
+
|
315
|
+
puts "\nā
Example 3 completed!"
|
316
|
+
end
|
317
|
+
|
318
|
+
# Example 4: Dynamic Initial State with Conditional Logic
|
319
|
+
def example_4_dynamic_initial_state
|
320
|
+
puts "\n" + "=" * 70
|
321
|
+
puts "š EXAMPLE 4: Dynamic Initial State with Conditional Logic"
|
322
|
+
puts "=" * 70
|
323
|
+
|
324
|
+
# Create a graph that behaves differently based on initial state
|
325
|
+
graph = LangGraphRB::Graph.new do
|
326
|
+
node :analyze_context do |state|
|
327
|
+
user_type = state[:user_type]
|
328
|
+
priority = state[:priority] || 'normal'
|
329
|
+
|
330
|
+
puts "š Analyzing context: user_type=#{user_type}, priority=#{priority}"
|
331
|
+
|
332
|
+
context = case user_type
|
333
|
+
when 'admin'
|
334
|
+
{ access_level: 'full', can_modify: true, queue_position: 0 }
|
335
|
+
when 'premium'
|
336
|
+
{ access_level: 'enhanced', can_modify: true, queue_position: 1 }
|
337
|
+
when 'standard'
|
338
|
+
{ access_level: 'basic', can_modify: false, queue_position: 2 }
|
339
|
+
else
|
340
|
+
{ access_level: 'guest', can_modify: false, queue_position: 3 }
|
341
|
+
end
|
342
|
+
|
343
|
+
# Priority affects queue position
|
344
|
+
if priority == 'high'
|
345
|
+
context[:queue_position] = [context[:queue_position] - 1, 0].max
|
346
|
+
end
|
347
|
+
|
348
|
+
context.merge({
|
349
|
+
context_analyzed: true,
|
350
|
+
analysis_time: Time.now
|
351
|
+
})
|
352
|
+
end
|
353
|
+
|
354
|
+
node :route_request do |state|
|
355
|
+
access_level = state[:access_level]
|
356
|
+
can_modify = state[:can_modify]
|
357
|
+
|
358
|
+
puts "š¦ Routing request for access_level: #{access_level}"
|
359
|
+
|
360
|
+
route = if access_level == 'full'
|
361
|
+
'admin_flow'
|
362
|
+
elsif can_modify
|
363
|
+
'user_flow'
|
364
|
+
else
|
365
|
+
'readonly_flow'
|
366
|
+
end
|
367
|
+
|
368
|
+
{
|
369
|
+
route: route,
|
370
|
+
routing_time: Time.now,
|
371
|
+
estimated_processing_time: state[:queue_position] * 0.5
|
372
|
+
}
|
373
|
+
end
|
374
|
+
|
375
|
+
node :process_request do |state|
|
376
|
+
route = state[:route]
|
377
|
+
|
378
|
+
puts "āļø Processing via #{route}"
|
379
|
+
|
380
|
+
result = case route
|
381
|
+
when 'admin_flow'
|
382
|
+
'Full admin access granted - all operations available'
|
383
|
+
when 'user_flow'
|
384
|
+
'User access granted - modification operations available'
|
385
|
+
when 'readonly_flow'
|
386
|
+
'Read-only access granted - viewing operations only'
|
387
|
+
else
|
388
|
+
'Unknown route - access denied'
|
389
|
+
end
|
390
|
+
|
391
|
+
{
|
392
|
+
result: result,
|
393
|
+
processed_at: Time.now,
|
394
|
+
success: true
|
395
|
+
}
|
396
|
+
end
|
397
|
+
|
398
|
+
set_entry_point :analyze_context
|
399
|
+
edge :analyze_context, :route_request
|
400
|
+
edge :route_request, :process_request
|
401
|
+
set_finish_point :process_request
|
402
|
+
end
|
403
|
+
|
404
|
+
graph.compile!
|
405
|
+
|
406
|
+
# Test different user scenarios with varying initial states
|
407
|
+
user_scenarios = [
|
408
|
+
{
|
409
|
+
name: "Admin User (High Priority)",
|
410
|
+
initial_state: { user_type: 'admin', priority: 'high', request_id: 'REQ-001' }
|
411
|
+
},
|
412
|
+
{
|
413
|
+
name: "Premium User (Normal Priority)",
|
414
|
+
initial_state: { user_type: 'premium', priority: 'normal', request_id: 'REQ-002' }
|
415
|
+
},
|
416
|
+
{
|
417
|
+
name: "Standard User (Low Priority)",
|
418
|
+
initial_state: { user_type: 'standard', priority: 'low', request_id: 'REQ-003' }
|
419
|
+
},
|
420
|
+
{
|
421
|
+
name: "Guest User (No Priority Set)",
|
422
|
+
initial_state: { user_type: 'guest', request_id: 'REQ-004' }
|
423
|
+
},
|
424
|
+
{
|
425
|
+
name: "Unknown User Type",
|
426
|
+
initial_state: { user_type: 'unknown', priority: 'high', request_id: 'REQ-005' }
|
427
|
+
}
|
428
|
+
]
|
429
|
+
|
430
|
+
user_scenarios.each_with_index do |scenario, i|
|
431
|
+
puts "\n--- #{scenario[:name]} ---"
|
432
|
+
puts "Initial state: #{scenario[:initial_state]}"
|
433
|
+
|
434
|
+
result = graph.invoke(scenario[:initial_state])
|
435
|
+
|
436
|
+
puts "Access level: #{result[:access_level]}"
|
437
|
+
puts "Can modify: #{result[:can_modify]}"
|
438
|
+
puts "Queue position: #{result[:queue_position]}"
|
439
|
+
puts "Route: #{result[:route]}"
|
440
|
+
puts "Result: #{result[:result]}"
|
441
|
+
puts "Est. processing time: #{result[:estimated_processing_time]}s"
|
442
|
+
end
|
443
|
+
|
444
|
+
puts "\nā
Example 4 completed!"
|
445
|
+
end
|
446
|
+
|
447
|
+
# Example 5: Initial State Validation and Error Handling
|
448
|
+
def example_5_state_validation
|
449
|
+
puts "\n" + "=" * 70
|
450
|
+
puts "ā
EXAMPLE 5: Initial State Validation and Error Handling"
|
451
|
+
puts "=" * 70
|
452
|
+
|
453
|
+
# Create a graph with comprehensive state validation
|
454
|
+
graph = LangGraphRB::Graph.new do
|
455
|
+
node :validate_required_fields do |state|
|
456
|
+
puts "š Validating required fields"
|
457
|
+
|
458
|
+
required_fields = [:user_id, :action, :timestamp]
|
459
|
+
missing_fields = required_fields.select { |field| state[field].nil? }
|
460
|
+
|
461
|
+
if missing_fields.any?
|
462
|
+
{
|
463
|
+
valid: false,
|
464
|
+
error: "Missing required fields: #{missing_fields.join(', ')}",
|
465
|
+
error_type: 'validation_error'
|
466
|
+
}
|
467
|
+
else
|
468
|
+
{
|
469
|
+
valid: true,
|
470
|
+
validated_at: Time.now,
|
471
|
+
validation_passed: true
|
472
|
+
}
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
node :validate_data_types do |state|
|
477
|
+
unless state[:valid] # Skip if already invalid
|
478
|
+
state
|
479
|
+
else
|
480
|
+
puts "š¢ Validating data types"
|
481
|
+
|
482
|
+
errors = []
|
483
|
+
|
484
|
+
# Validate user_id is numeric
|
485
|
+
unless state[:user_id].is_a?(Numeric) || state[:user_id].to_s.match?(/^\d+$/)
|
486
|
+
errors << "user_id must be numeric"
|
487
|
+
end
|
488
|
+
|
489
|
+
# Validate action is a valid string
|
490
|
+
unless state[:action].is_a?(String) && !state[:action].strip.empty?
|
491
|
+
errors << "action must be a non-empty string"
|
492
|
+
end
|
493
|
+
|
494
|
+
# Validate timestamp is a time-like object
|
495
|
+
unless state[:timestamp].is_a?(Time)
|
496
|
+
begin
|
497
|
+
Time.parse(state[:timestamp].to_s) if state[:timestamp]
|
498
|
+
rescue
|
499
|
+
errors << "timestamp must be a valid time format"
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
if errors.any?
|
504
|
+
{
|
505
|
+
valid: false,
|
506
|
+
error: "Data type validation failed: #{errors.join(', ')}",
|
507
|
+
error_type: 'type_error'
|
508
|
+
}
|
509
|
+
else
|
510
|
+
{
|
511
|
+
type_validation_passed: true,
|
512
|
+
all_validations_passed: true
|
513
|
+
}
|
514
|
+
end
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
node :process_valid_state do |state|
|
519
|
+
unless state[:all_validations_passed]
|
520
|
+
state
|
521
|
+
else
|
522
|
+
puts "ā
Processing valid state"
|
523
|
+
|
524
|
+
{
|
525
|
+
processed: true,
|
526
|
+
user_id: state[:user_id].to_i,
|
527
|
+
action: state[:action].upcase,
|
528
|
+
parsed_timestamp: state[:timestamp].is_a?(Time) ? state[:timestamp] : Time.parse(state[:timestamp].to_s),
|
529
|
+
processing_completed_at: Time.now,
|
530
|
+
success: true
|
531
|
+
}
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
node :handle_error do |state|
|
536
|
+
if state[:success] # Skip if processing succeeded
|
537
|
+
state
|
538
|
+
else
|
539
|
+
puts "ā Handling validation error"
|
540
|
+
|
541
|
+
{
|
542
|
+
error_handled: true,
|
543
|
+
error_logged_at: Time.now,
|
544
|
+
suggested_fix: case state[:error_type]
|
545
|
+
when 'validation_error'
|
546
|
+
'Please ensure all required fields are provided'
|
547
|
+
when 'type_error'
|
548
|
+
'Please check data types match expected formats'
|
549
|
+
else
|
550
|
+
'Please review your input data'
|
551
|
+
end
|
552
|
+
}
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
set_entry_point :validate_required_fields
|
557
|
+
edge :validate_required_fields, :validate_data_types
|
558
|
+
edge :validate_data_types, :process_valid_state
|
559
|
+
edge :process_valid_state, :handle_error
|
560
|
+
set_finish_point :handle_error
|
561
|
+
end
|
562
|
+
|
563
|
+
graph.compile!
|
564
|
+
|
565
|
+
# Test various initial state scenarios - valid and invalid
|
566
|
+
test_scenarios = [
|
567
|
+
{
|
568
|
+
name: "Valid State",
|
569
|
+
state: { user_id: 123, action: "login", timestamp: "2024-01-15 10:30:00" }
|
570
|
+
},
|
571
|
+
{
|
572
|
+
name: "Missing Required Field",
|
573
|
+
state: { user_id: 123, action: "login" } # missing timestamp
|
574
|
+
},
|
575
|
+
{
|
576
|
+
name: "Invalid Data Types",
|
577
|
+
state: { user_id: "not-a-number", action: "", timestamp: "invalid-time" }
|
578
|
+
},
|
579
|
+
{
|
580
|
+
name: "Completely Empty State",
|
581
|
+
state: {}
|
582
|
+
},
|
583
|
+
{
|
584
|
+
name: "Partial Valid State",
|
585
|
+
state: { user_id: "456", action: "logout", timestamp: Time.now }
|
586
|
+
}
|
587
|
+
]
|
588
|
+
|
589
|
+
test_scenarios.each do |scenario|
|
590
|
+
puts "\n--- Testing: #{scenario[:name]} ---"
|
591
|
+
puts "Input: #{scenario[:state]}"
|
592
|
+
|
593
|
+
result = graph.invoke(scenario[:state])
|
594
|
+
|
595
|
+
if result[:success]
|
596
|
+
puts "ā
SUCCESS!"
|
597
|
+
puts " Processed user_id: #{result[:user_id]}"
|
598
|
+
puts " Processed action: #{result[:action]}"
|
599
|
+
puts " Parsed timestamp: #{result[:parsed_timestamp]}"
|
600
|
+
else
|
601
|
+
puts "ā VALIDATION FAILED!"
|
602
|
+
puts " Error: #{result[:error]}"
|
603
|
+
puts " Error type: #{result[:error_type]}"
|
604
|
+
puts " Suggested fix: #{result[:suggested_fix]}" if result[:suggested_fix]
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
puts "\nā
Example 5 completed!"
|
609
|
+
end
|
610
|
+
|
611
|
+
# Main execution
|
612
|
+
def main
|
613
|
+
puts "š LangGraphRB Initial State Examples"
|
614
|
+
puts "======================================"
|
615
|
+
puts
|
616
|
+
puts "This example demonstrates various ways to work with initial state:"
|
617
|
+
puts "1. Basic initial state with simple hash"
|
618
|
+
puts "2. Initial state with custom reducers"
|
619
|
+
puts "3. Advanced initial state with custom state class"
|
620
|
+
puts "4. Dynamic initial state with conditional logic"
|
621
|
+
puts "5. Initial state validation and error handling"
|
622
|
+
puts
|
623
|
+
|
624
|
+
example_1_basic_initial_state
|
625
|
+
example_2_custom_reducers
|
626
|
+
example_3_custom_state_class
|
627
|
+
example_4_dynamic_initial_state
|
628
|
+
example_5_state_validation
|
629
|
+
|
630
|
+
puts "\n" + "=" * 70
|
631
|
+
puts "š All Initial State Examples Completed!"
|
632
|
+
puts "=" * 70
|
633
|
+
puts
|
634
|
+
puts "Key Takeaways:"
|
635
|
+
puts "⢠Initial state can be a simple hash passed to graph.invoke()"
|
636
|
+
puts "⢠Use reducers to control how state updates are merged"
|
637
|
+
puts "⢠Custom state classes can provide default reducers"
|
638
|
+
puts "⢠Initial state affects graph execution flow and routing"
|
639
|
+
puts "⢠Always validate initial state for robust applications"
|
640
|
+
puts "⢠LangGraphRB::State provides common reducers like add_messages"
|
641
|
+
end
|
642
|
+
|
643
|
+
# Run the examples
|
644
|
+
if __FILE__ == $0
|
645
|
+
main
|
646
|
+
end
|