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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d21ea984522f684793c24a7be4f2ccd5c90dfad98512647949e42b9e2b8c1dee
4
- data.tar.gz: 99b3ebfdc7a42fb86cee874d3471f3dc21a728eba018497c695acc7f9af50611
3
+ metadata.gz: 9c2d260c430ce9be20fea65d6dff4d8013a535a6db46cdd89e15b19200c8b6dd
4
+ data.tar.gz: a5213444bbf90954158836c6e73237752ecc2d689e0e4de4807621edb2460f44
5
5
  SHA512:
6
- metadata.gz: 94c90d4d802c354776a8440662bb9f50af883b78baacd9891bb6f43c76052540a0a7767facc238931c53c4752a0802787dd82ba277b370e19049a4939b7d5c0e
7
- data.tar.gz: c12b92e058b0eccf5150d4491e603746abadb946386d7b0e51379d5ec481f8e1f01512023d8f0e492adf203355c5450a2b32aded362840efe4bb1be9ee11088c
6
+ metadata.gz: ce7b663fb858e5c2adea131d21682df32eddc0cbb72b324773fa0b1cf932338c105d155e7b7b046556748e9f5d60342b9171537a21e1f41c7f7e4933499968cb
7
+ data.tar.gz: e30e5ad223be3a455361f753b44ba928054eff285429ca0a8c73474eb851eb9c6c59c627357010032d5133d8ba9d891669225e3640aea5873017910724766536
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ # Gem artifacts
2
+ *.gem
3
+
4
+ # Other common Ruby ignores
5
+ .bundle/
6
+ vendor/bundle/
7
+ .ruby-version
8
+ .byebug_history
9
+ coverage/
10
+ *.log
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
- - [ ] Metrics and observability features
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