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.
@@ -0,0 +1,71 @@
1
+ require 'logger'
2
+
3
+ module LangGraphRB
4
+ module Observers
5
+ # File and stdout logging observer
6
+ class LoggerObserver < BaseObserver
7
+ def initialize(logger: nil, level: :info, format: :text)
8
+ @logger = logger || Logger.new($stdout)
9
+ @level = level
10
+ @format = format
11
+ @logger.level = Logger.const_get(level.to_s.upcase)
12
+ end
13
+
14
+ def on_graph_start(event)
15
+ log(:info, "Graph execution started", event.to_h)
16
+ end
17
+
18
+ def on_graph_end(event)
19
+ log(:info, "Graph execution completed", event.to_h)
20
+ end
21
+
22
+ def on_node_start(event)
23
+ log(:debug, "Node execution started: #{event.node_name}", event.to_h)
24
+ end
25
+
26
+ def on_node_end(event)
27
+ duration_ms = event.duration ? (event.duration * 1000).round(2) : 'unknown'
28
+ log(:info, "Node completed: #{event.node_name} (#{duration_ms}ms)", event.to_h)
29
+ end
30
+
31
+ def on_node_error(event)
32
+ log(:error, "Node error: #{event.node_name} - #{event.error&.message}", event.to_h)
33
+ end
34
+
35
+ def on_step_complete(event)
36
+ log(:debug, "Step #{event.step_number} completed with #{event.completed_nodes.length} nodes", event.to_h)
37
+ end
38
+
39
+ def on_interrupt(event)
40
+ log(:warn, "Execution interrupted: #{event[:message]}", event)
41
+ end
42
+
43
+ private
44
+
45
+ def log(level, message, data)
46
+ case @format
47
+ when :json
48
+ @logger.send(level, { message: message, data: sanitize_data(data) }.to_json)
49
+ else
50
+ @logger.send(level, "#{message} | #{format_data(data)}")
51
+ end
52
+ end
53
+
54
+ def format_data(data)
55
+ relevant_fields = data.select { |k, v| [:thread_id, :step_number, :node_name, :duration_ms].include?(k) && v }
56
+ relevant_fields.map { |k, v| "#{k}=#{v}" }.join(" ")
57
+ end
58
+
59
+ def sanitize_data(data)
60
+ data.transform_values do |value|
61
+ case value
62
+ when Hash
63
+ sanitize_state(value, max_size: 500)
64
+ else
65
+ value
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,111 @@
1
+ require 'json'
2
+
3
+ module LangGraphRB
4
+ module Observers
5
+ # Structured data observer for APM/monitoring integration
6
+ class StructuredObserver < BaseObserver
7
+ def initialize(sink: nil, format: :json, include_state: false, async: false)
8
+ @sink = sink || $stdout
9
+ @format = format
10
+ @include_state = include_state
11
+ @async = async
12
+ @event_queue = async ? Queue.new : nil
13
+ @worker_thread = start_worker_thread if async
14
+ end
15
+
16
+ def on_graph_start(event)
17
+ emit_event(:graph_start, event.to_h)
18
+ end
19
+
20
+ def on_graph_end(event)
21
+ emit_event(:graph_end, event.to_h)
22
+ end
23
+
24
+ def on_node_start(event)
25
+ emit_event(:node_start, sanitize_event(event.to_h))
26
+ end
27
+
28
+ def on_node_end(event)
29
+ emit_event(:node_end, sanitize_event(event.to_h))
30
+ end
31
+
32
+ def on_node_error(event)
33
+ emit_event(:node_error, sanitize_event(event.to_h))
34
+ end
35
+
36
+ def on_step_complete(event)
37
+ emit_event(:step_complete, sanitize_event(event.to_h))
38
+ end
39
+
40
+ def on_command_processed(event)
41
+ emit_event(:command_processed, event)
42
+ end
43
+
44
+ def shutdown
45
+ if @async && @worker_thread
46
+ @event_queue << :shutdown
47
+ @worker_thread.join
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def emit_event(type, data)
54
+ event = {
55
+ event_type: type,
56
+ timestamp: Time.now.utc.iso8601,
57
+ **data
58
+ }
59
+
60
+ if @async
61
+ @event_queue << event
62
+ else
63
+ write_event(event)
64
+ end
65
+ end
66
+
67
+ def write_event(event)
68
+ case @format
69
+ when :json
70
+ @sink.puts(JSON.generate(event))
71
+ when :ndjson
72
+ @sink.puts(JSON.generate(event))
73
+ else
74
+ @sink.puts(event.inspect)
75
+ end
76
+
77
+ @sink.flush if @sink.respond_to?(:flush)
78
+ end
79
+
80
+ def sanitize_event(event_data)
81
+ result = event_data.dup
82
+
83
+ unless @include_state
84
+ result.delete(:state_before)
85
+ result.delete(:state_after)
86
+ result.delete(:state)
87
+ else
88
+ result[:state_before] = sanitize_state(result[:state_before]) if result[:state_before]
89
+ result[:state_after] = sanitize_state(result[:state_after]) if result[:state_after]
90
+ result[:state] = sanitize_state(result[:state]) if result[:state]
91
+ end
92
+
93
+ result
94
+ end
95
+
96
+ def start_worker_thread
97
+ Thread.new do
98
+ loop do
99
+ event = @event_queue.pop
100
+ break if event == :shutdown
101
+
102
+ write_event(event)
103
+ rescue => e
104
+ # Log error but continue processing
105
+ $stderr.puts "Observer error: #{e.message}"
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -1,16 +1,19 @@
1
1
  require 'thread'
2
+ require 'json'
3
+ require 'time'
2
4
 
3
5
  module LangGraphRB
4
6
  class Runner
5
7
  attr_reader :graph, :store, :thread_id
6
8
 
7
- def initialize(graph, store:, thread_id:)
9
+ def initialize(graph, store:, thread_id:, observers: [])
8
10
  @graph = graph
9
11
  @store = store
10
12
  @thread_id = thread_id
11
13
  @step_number = 0
12
14
  @execution_queue = Queue.new
13
15
  @interrupt_handler = nil
16
+ @observers = Array(observers)
14
17
  end
15
18
 
16
19
  # Synchronous execution
@@ -26,6 +29,8 @@ module LangGraphRB
26
29
 
27
30
  # Streaming execution with optional block for receiving intermediate results
28
31
  def stream(initial_state, context: nil, &block)
32
+ notify_graph_start(initial_state, context)
33
+
29
34
  @step_number = 0
30
35
  current_state = initial_state
31
36
 
@@ -124,11 +129,17 @@ module LangGraphRB
124
129
  break if final_state
125
130
  end
126
131
 
127
- {
132
+ result = {
128
133
  state: current_state,
129
134
  step_number: @step_number,
130
135
  thread_id: @thread_id
131
136
  }
137
+
138
+ notify_graph_end(current_state)
139
+ result
140
+ rescue => error
141
+ notify_graph_end(current_state || initial_state)
142
+ raise
132
143
  end
133
144
 
134
145
  # Resume from checkpoint
@@ -150,6 +161,79 @@ module LangGraphRB
150
161
 
151
162
  private
152
163
 
164
+ def notify_observers(method, event)
165
+ @observers.each do |observer|
166
+ begin
167
+ observer.send(method, event)
168
+ rescue => e
169
+ # Log observer errors but don't fail execution
170
+ $stderr.puts "Observer error in #{observer.class}##{method}: #{e.message}"
171
+ end
172
+ end
173
+ end
174
+
175
+ def notify_graph_start(initial_state, context)
176
+ event = Observers::GraphEvent.new(
177
+ type: :start,
178
+ graph: @graph,
179
+ initial_state: initial_state,
180
+ context: context,
181
+ thread_id: @thread_id
182
+ )
183
+ notify_observers(:on_graph_start, event)
184
+ end
185
+
186
+ def notify_graph_end(final_state)
187
+ event = Observers::GraphEvent.new(
188
+ type: :end,
189
+ graph: @graph,
190
+ initial_state: final_state,
191
+ thread_id: @thread_id
192
+ )
193
+ notify_observers(:on_graph_end, event)
194
+ end
195
+
196
+ def notify_node_start(node, state, context)
197
+ event = Observers::NodeEvent.new(
198
+ type: :start,
199
+ node_name: node.name,
200
+ node_class: node.class,
201
+ state_before: state,
202
+ context: context,
203
+ thread_id: @thread_id,
204
+ step_number: @step_number
205
+ )
206
+ notify_observers(:on_node_start, event)
207
+ end
208
+
209
+ def notify_node_end(node, state_before, state_after, result, duration)
210
+ event = Observers::NodeEvent.new(
211
+ type: :end,
212
+ node_name: node.name,
213
+ node_class: node.class,
214
+ state_before: state_before,
215
+ state_after: state_after,
216
+ result: result,
217
+ duration: duration,
218
+ thread_id: @thread_id,
219
+ step_number: @step_number
220
+ )
221
+ notify_observers(:on_node_end, event)
222
+ end
223
+
224
+ def notify_node_error(node, state, error)
225
+ event = Observers::NodeEvent.new(
226
+ type: :error,
227
+ node_name: node.name,
228
+ node_class: node.class,
229
+ state_before: state,
230
+ error: error,
231
+ thread_id: @thread_id,
232
+ step_number: @step_number
233
+ )
234
+ notify_observers(:on_node_error, event)
235
+ end
236
+
153
237
  # Execute all nodes in the current super-step in parallel
154
238
  def execute_super_step(active_executions, context)
155
239
  return [] if active_executions.empty?
@@ -184,10 +268,29 @@ module LangGraphRB
184
268
 
185
269
  # Safely execute a single node
186
270
  def execute_node_safely(node, state, context, step)
271
+ notify_node_start(node, state, context)
272
+
273
+ start_time = Time.now
187
274
  begin
188
275
  result = node.call(state, context: context)
189
- process_node_result(node.name, state, result, step)
276
+ duration = Time.now - start_time
277
+
278
+ processed_result = process_node_result(node.name, state, result, step)
279
+
280
+ # Extract final state from processed result
281
+ final_state = case processed_result[:type]
282
+ when :completed
283
+ processed_result[:state]
284
+ else
285
+ state
286
+ end
287
+
288
+ notify_node_end(node, state, final_state, result, duration)
289
+ processed_result
190
290
  rescue => error
291
+ duration = Time.now - start_time
292
+ notify_node_error(node, state, error)
293
+
191
294
  {
192
295
  type: :error,
193
296
  node_name: node.name,
@@ -1,3 +1,3 @@
1
1
  module LangGraphRB
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
data/lib/langgraph_rb.rb CHANGED
@@ -6,10 +6,14 @@ require_relative 'langgraph_rb/command'
6
6
  require_relative 'langgraph_rb/graph'
7
7
  require_relative 'langgraph_rb/runner'
8
8
  require_relative 'langgraph_rb/stores/memory'
9
+ require_relative 'langgraph_rb/observers/base'
10
+ require_relative 'langgraph_rb/observers/logger'
11
+ require_relative 'langgraph_rb/observers/structured'
9
12
 
10
13
  module LangGraphRB
11
14
  class Error < StandardError; end
12
15
  class GraphError < Error; end
13
16
  class NodeError < Error; end
14
17
  class StateError < Error; end
18
+ class ObserverError < Error; end
15
19
  end
data/test_runner.rb CHANGED
@@ -67,18 +67,18 @@ routing_graph = LangGraphRB::Graph.new do
67
67
  { message: "Number #{state[:number]} is positive!" }
68
68
  end
69
69
 
70
- node :negative_handler do |state|
70
+ node :generate_sql do |state|
71
71
  { message: "Number #{state[:number]} is negative or zero!" }
72
72
  end
73
73
 
74
74
  set_entry_point :check_number
75
75
 
76
76
  conditional_edge :check_number, ->(state) {
77
- state[:is_positive] ? :positive_handler : :negative_handler
77
+ state[:is_positive] ? :positive_handler : :generate_sql
78
78
  }
79
79
 
80
80
  set_finish_point :positive_handler
81
- set_finish_point :negative_handler
81
+ set_finish_point :generate_sql
82
82
  end
83
83
 
84
84
  routing_graph.compile!
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: langgraph_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Toro
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-29 00:00:00.000000000 Z
11
+ date: 2025-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -104,11 +104,15 @@ executables: []
104
104
  extensions: []
105
105
  extra_rdoc_files: []
106
106
  files:
107
+ - ".gitignore"
107
108
  - Gemfile
108
109
  - README.md
109
110
  - SUMMARY.md
110
111
  - examples/advanced_example.rb
111
112
  - examples/basic_example.rb
113
+ - examples/initial_state_example.rb
114
+ - examples/observer_example.rb
115
+ - examples/reducers_example.rb
112
116
  - examples/simple_test.rb
113
117
  - langgraph_rb.gemspec
114
118
  - lib/langgraph_rb.rb
@@ -116,6 +120,9 @@ files:
116
120
  - lib/langgraph_rb/edge.rb
117
121
  - lib/langgraph_rb/graph.rb
118
122
  - lib/langgraph_rb/node.rb
123
+ - lib/langgraph_rb/observers/base.rb
124
+ - lib/langgraph_rb/observers/logger.rb
125
+ - lib/langgraph_rb/observers/structured.rb
119
126
  - lib/langgraph_rb/runner.rb
120
127
  - lib/langgraph_rb/state.rb
121
128
  - lib/langgraph_rb/stores/memory.rb