langgraph_rb 0.1.1 → 0.1.3

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: 9c2d260c430ce9be20fea65d6dff4d8013a535a6db46cdd89e15b19200c8b6dd
4
- data.tar.gz: a5213444bbf90954158836c6e73237752ecc2d689e0e4de4807621edb2460f44
3
+ metadata.gz: 7629f7f9d5f4778ec860440939aa791d896fe0db95dc4d2b454aa06b4f6e6986
4
+ data.tar.gz: 1e607cbc2ea8238f8d06228572e8fc782d61fc68a1999310e0180da1e490a36a
5
5
  SHA512:
6
- metadata.gz: ce7b663fb858e5c2adea131d21682df32eddc0cbb72b324773fa0b1cf932338c105d155e7b7b046556748e9f5d60342b9171537a21e1f41c7f7e4933499968cb
7
- data.tar.gz: e30e5ad223be3a455361f753b44ba928054eff285429ca0a8c73474eb851eb9c6c59c627357010032d5133d8ba9d891669225e3640aea5873017910724766536
6
+ metadata.gz: 3bb81a69fbe5288500d87b7d166365f4fbebd6469c234ba3772e06e22ce685e1bd5aa07a649931e01b2799132071c69e9afb319781c5c0c5da7a7ba86424a197
7
+ data.tar.gz: 83f0fc66a0309602c334be73ab07d3352cc537f82b5942c23983bc4b0ea343f4f50f81c5e1055b7cad32f5dbea69bc5a3654dd8a4229e71cbd3b13383c9faaf8
data/.gitignore CHANGED
@@ -7,4 +7,5 @@ vendor/bundle/
7
7
  .ruby-version
8
8
  .byebug_history
9
9
  coverage/
10
- *.log
10
+ *.log
11
+ .env
data/Gemfile CHANGED
@@ -6,4 +6,5 @@ group :development, :test do
6
6
  gem 'rspec', '~> 3.0'
7
7
  gem 'pry', '~> 0.14'
8
8
  gem 'rubocop', '~> 1.0'
9
+ gem 'langfuse', '~> 0.1'
9
10
  end
data/Gemfile.lock ADDED
@@ -0,0 +1,80 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ langgraph_rb (0.1.2)
5
+ json (~> 2.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.3)
11
+ coderay (1.1.3)
12
+ concurrent-ruby (1.3.5)
13
+ diff-lcs (1.6.2)
14
+ json (2.13.2)
15
+ langfuse (0.1.1)
16
+ concurrent-ruby (~> 1.2)
17
+ sorbet-runtime (~> 0.5)
18
+ language_server-protocol (3.17.0.5)
19
+ lint_roller (1.1.0)
20
+ method_source (1.1.0)
21
+ parallel (1.27.0)
22
+ parser (3.3.9.0)
23
+ ast (~> 2.4.1)
24
+ racc
25
+ prism (1.4.0)
26
+ pry (0.15.2)
27
+ coderay (~> 1.1)
28
+ method_source (~> 1.0)
29
+ racc (1.8.1)
30
+ rainbow (3.1.1)
31
+ rake (13.3.0)
32
+ regexp_parser (2.11.1)
33
+ rspec (3.13.1)
34
+ rspec-core (~> 3.13.0)
35
+ rspec-expectations (~> 3.13.0)
36
+ rspec-mocks (~> 3.13.0)
37
+ rspec-core (3.13.5)
38
+ rspec-support (~> 3.13.0)
39
+ rspec-expectations (3.13.5)
40
+ diff-lcs (>= 1.2.0, < 2.0)
41
+ rspec-support (~> 3.13.0)
42
+ rspec-mocks (3.13.5)
43
+ diff-lcs (>= 1.2.0, < 2.0)
44
+ rspec-support (~> 3.13.0)
45
+ rspec-support (3.13.4)
46
+ rubocop (1.79.2)
47
+ json (~> 2.3)
48
+ language_server-protocol (~> 3.17.0.2)
49
+ lint_roller (~> 1.1.0)
50
+ parallel (~> 1.10)
51
+ parser (>= 3.3.0.2)
52
+ rainbow (>= 2.2.2, < 4.0)
53
+ regexp_parser (>= 2.9.3, < 3.0)
54
+ rubocop-ast (>= 1.46.0, < 2.0)
55
+ ruby-progressbar (~> 1.7)
56
+ unicode-display_width (>= 2.4.0, < 4.0)
57
+ rubocop-ast (1.46.0)
58
+ parser (>= 3.3.7.2)
59
+ prism (~> 1.4)
60
+ ruby-progressbar (1.13.0)
61
+ sorbet-runtime (0.6.12534)
62
+ unicode-display_width (3.1.4)
63
+ unicode-emoji (~> 4.0, >= 4.0.4)
64
+ unicode-emoji (4.0.4)
65
+
66
+ PLATFORMS
67
+ arm64-darwin-22
68
+ ruby
69
+
70
+ DEPENDENCIES
71
+ bundler (~> 2.0)
72
+ langfuse (~> 0.1)
73
+ langgraph_rb!
74
+ pry (~> 0.14)
75
+ rake (~> 13.0)
76
+ rspec (~> 3.0)
77
+ rubocop (~> 1.0)
78
+
79
+ BUNDLED WITH
80
+ 2.6.7
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby
2
+ require 'langfuse'
3
+ require_relative '../lib/langgraph_rb'
4
+
5
+ url = 'https://us.cloud.langfuse.com'
6
+
7
+ Langfuse.configure do |config|
8
+ config.public_key = ENV['LANGFUSE_PUBLIC_KEY'] # e.g., 'pk-lf-...'
9
+ config.secret_key = ENV['LANGFUSE_SECRET_KEY'] # e.g., 'sk-lf-...'
10
+ config.host = url
11
+ config.debug = true # Enable debug logging
12
+ end
13
+
14
+
15
+ class LangfuseObserver < LangGraphRB::Observers::BaseObserver
16
+
17
+ def on_graph_start(event)
18
+ @trace ||= Langfuse.trace(
19
+ name: "graph-start2",
20
+ thread_id: event.thread_id,
21
+ metadata: event.to_h
22
+ )
23
+ end
24
+
25
+ def on_node_end(event)
26
+ span = Langfuse.span(
27
+ name: "node-#{event.node_name}",
28
+ trace_id: @trace.id,
29
+ input: event.to_h,
30
+ )
31
+ Langfuse.update_span(span)
32
+ end
33
+ end
34
+
35
+
36
+ def langfuse_example
37
+ puts "########################################################"
38
+ puts "########################################################"
39
+ puts "########################################################"
40
+ puts "=== Langfuse Example ==="
41
+
42
+ # Create a simple graph for demonstration
43
+
44
+ graph = LangGraphRB::Graph.new(state_class: LangGraphRB::State) do
45
+ node :process_message do |state|
46
+ sleep(Random.rand(0.1..0.5))
47
+ { message: "Processed: #{state[:message]}" }
48
+ end
49
+
50
+ conditional_edge :process_message, -> (state) {
51
+ sleep(Random.rand(0.1..0.5))
52
+ if state[:value] > 0 and state[:value] < 10
53
+ puts "Processed between 0 and 10"
54
+ return :process_between_0_and_10
55
+ elsif state[:value] > 10
56
+ puts "Processed greater than 10"
57
+ return :process_greater_than_10
58
+ else
59
+ puts "Processed less than 0"
60
+ return :process_less_than_0
61
+ end
62
+ }
63
+
64
+ node :process_between_0_and_10 do |state|
65
+ { message: "Processed between 0 and 10: #{state[:message]}" }
66
+ end
67
+
68
+ node :process_greater_than_10 do |state|
69
+ { message: "Processed greater than 10: #{state[:message]}" }
70
+ end
71
+
72
+ node :process_less_than_0 do |state|
73
+ { message: "Processed less than 0: #{state[:message]}" }
74
+ end
75
+
76
+ set_entry_point :process_message
77
+ set_finish_point :process_between_0_and_10
78
+ set_finish_point :process_greater_than_10
79
+ set_finish_point :process_less_than_0
80
+ end
81
+
82
+
83
+
84
+ graph.compile!
85
+ result = graph.invoke({ message: "Hello World", value: 31}, observers: [LangfuseObserver.new])
86
+ puts "Result: #{result}"
87
+ puts "########################################################"
88
+ puts "########################################################"
89
+ puts "########################################################"
90
+ puts "########################################################"
91
+ end
92
+
93
+ langfuse_example
94
+
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env ruby
2
+ require 'langfuse'
3
+ require_relative '../lib/langgraph_rb'
4
+
5
+ url = 'https://us.cloud.langfuse.com'
6
+
7
+ puts "LANGFUSE_PUBLIC_KEY: #{ENV['LANGFUSE_PUBLIC_KEY']}"
8
+ puts "LANGFUSE_SECRET_KEY: #{ENV['LANGFUSE_SECRET_KEY']}"
9
+ puts "LANGFUSE_HOST: #{url}"
10
+ puts "LANGFUSE_DEBUG: #{true}"
11
+
12
+ Langfuse.configure do |config|
13
+ config.public_key = ENV['LANGFUSE_PUBLIC_KEY'] # e.g., 'pk-lf-...'
14
+ config.secret_key = ENV['LANGFUSE_SECRET_KEY'] # e.g., 'sk-lf-...'
15
+ config.host = url
16
+ config.debug = true # Enable debug logging
17
+ end
18
+
19
+ # Very simple mock LLM client. Bring your own real client instead.
20
+ class MockLLMClient
21
+
22
+ def set_observers(observers, node_name)
23
+ @observers = observers
24
+ @node_name = node_name
25
+ end
26
+
27
+ def call(messages)
28
+
29
+ data = {
30
+ name: "MockLLMClient",
31
+ model: "MockLLM",
32
+ model_parameters: {
33
+ temperature: 0.5,
34
+ max_tokens: 1000
35
+ },
36
+ input: messages,
37
+ }
38
+
39
+ log_llm_request(data)
40
+
41
+ last_user_message = messages.reverse.find { |m| m[:role] == 'user' }&.dig(:content)
42
+ "(mock) You said: #{last_user_message}"
43
+
44
+ data = {
45
+ output: "(mock) You said: #{last_user_message}",
46
+ prompt_tokens: 100,
47
+ completion_tokens: 100,
48
+ total_tokens: 200,
49
+ }
50
+
51
+ log_llm_response(data)
52
+ end
53
+
54
+ def log_llm_request(data)
55
+ @observer&.each do |observer|
56
+ observer.on_llm_request(data, @node_name)
57
+ end
58
+ end
59
+
60
+ def log_llm_response(data)
61
+ @observers&.each do |observer|
62
+ observer.on_llm_response(data, @node_name)
63
+ end
64
+ end
65
+ end
66
+
67
+ class LangfuseObserver < LangGraphRB::Observers::BaseObserver
68
+
69
+ def initialize
70
+ @trace = nil
71
+ @spans_by_node = {}
72
+ end
73
+
74
+ def on_graph_start(event)
75
+ @trace ||= Langfuse.trace(
76
+ name: "llm-graph",
77
+ thread_id: event.thread_id,
78
+ metadata: event.to_h
79
+ )
80
+ end
81
+
82
+ def on_node_start(event)
83
+ @spans_by_node[event.node_name] ||= {
84
+ span: Langfuse.span(
85
+ name: "node-#{event.node_name}",
86
+ trace_id: @trace.id,
87
+ input: event.to_h,
88
+ ),
89
+ generation: nil
90
+ }
91
+ Langfuse.update_span(@spans_by_node[event.node_name][:span])
92
+ end
93
+
94
+ def on_node_end(event)
95
+ # @spans_by_node[event.node_name] ||= {
96
+ # span: Langfuse.span(
97
+ # name: "node-#{event.node_name}",
98
+ # trace_id: @trace.id,
99
+ # input: event.to_h,
100
+ # ),
101
+ # generation: nil
102
+ # }
103
+ # Langfuse.update_span(@spans_by_node[event.node_name][:span])
104
+ end
105
+
106
+ def on_llm_request(event, node_name)
107
+ puts "########################################################"
108
+ puts "on_llm_request: #{event}"
109
+ puts "node_name: #{node_name}"
110
+ puts "spans_by_node: #{@spans_by_node}"
111
+ puts "$$$$--------------------------------------------------------$$$$"
112
+ span = @spans_by_node[node_name][:span]
113
+ generation = Langfuse.generation(
114
+ name: event[:name],
115
+ trace_id: @trace.id,
116
+ parent_observation_id: span.id,
117
+ model: event[:model],
118
+ model_parameters: event[:model_parameters],
119
+ input: event[:input]
120
+ )
121
+
122
+ @spans_by_node[node_name.to_sym][:generation] = generation
123
+ end
124
+
125
+ def on_llm_response(event, node_name)
126
+ puts "########################################################"
127
+ puts "on_llm_response: #{event}"
128
+ puts "node_name: #{node_name}"
129
+ puts "spans_by_node: #{@spans_by_node}"
130
+ puts "$$$$--------------------------------------------------------$$$$"
131
+
132
+ generation = @spans_by_node[node_name][:generation]
133
+
134
+ return if generation.nil?
135
+
136
+ generation.output = event[:output]
137
+ generation.usage = Langfuse::Models::Usage.new(
138
+ prompt_tokens: event[:prompt_tokens],
139
+ completion_tokens: event[:completion_tokens],
140
+ total_tokens: event[:total_tokens]
141
+ )
142
+ Langfuse.update_generation(generation)
143
+
144
+ @spans_by_node[node_name.to_sym][:generation] = nil
145
+ end
146
+ end
147
+
148
+ def llmnode_example
149
+ puts "=== LLMNode Example ==="
150
+
151
+ mock_llm = MockLLMClient.new
152
+
153
+ # Build a minimal chat graph using an LLM node.
154
+ graph = LangGraphRB::Graph.new(state_class: LangGraphRB::State) do
155
+ # Collect user input into the message history
156
+ node :receive_input do |state|
157
+ user_msg = { role: 'user', content: state[:input].to_s }
158
+ existing = state[:messages] || []
159
+ { messages: existing + [user_msg], last_user_message: state[:input].to_s }
160
+ end
161
+
162
+ # LLM node – uses a custom block to call the provided client via context
163
+ # Note: The default LLM behavior can be used once the core library wires a default callable.
164
+ llm_node :chat, llm_client: mock_llm, system_prompt: "You are a helpful assistant." do |state, context|
165
+ messages = state[:messages] || []
166
+
167
+ puts "########################################################"
168
+ puts "########################################################"
169
+
170
+ puts "context: #{context}"
171
+
172
+ puts "########################################################"
173
+ puts "########################################################"
174
+
175
+ # Optionally prepend a system prompt
176
+ if context[:system_prompt]
177
+ messages = [{ role: 'system', content: context[:system_prompt] }] + messages
178
+ end
179
+
180
+ response = context[:llm_client].call(messages)
181
+
182
+ assistant_msg = { role: 'assistant', content: response }
183
+ { messages: (state[:messages] || []) + [assistant_msg], last_response: response }
184
+ end
185
+
186
+ set_entry_point :receive_input
187
+ edge :receive_input, :chat
188
+ set_finish_point :chat
189
+ end
190
+
191
+ graph.compile!
192
+
193
+ # Single-turn example
194
+ result = graph.invoke({ messages: [], input: "Hello there!" }, observers: [LangfuseObserver.new])
195
+
196
+ puts "Assistant: #{result[:last_response]}"
197
+ puts "Messages:"
198
+ (result[:messages] || []).each { |m| puts " - #{m[:role]}: #{m[:content]}" }
199
+
200
+ # Multi-turn example (reuse message history)
201
+ second = graph.invoke({ messages: result[:messages], input: "What's the weather like?" })
202
+
203
+ puts "\nAssistant (turn 2): #{second[:last_response]}"
204
+ puts "Messages (after 2 turns):"
205
+ (second[:messages] || []).each { |m| puts " - #{m[:role]}: #{m[:content]}" }
206
+ end
207
+
208
+ llmnode_example
209
+
210
+
@@ -40,27 +40,47 @@ module LangGraphRB
40
40
  def initialize(name, llm_client:, system_prompt: nil, &block)
41
41
  @llm_client = llm_client
42
42
  @system_prompt = system_prompt
43
-
44
- super(name, &block)
43
+
44
+ # Use default LLM behavior if no custom block provided
45
+ super(name, &(block || method(:default_llm_call)))
45
46
  end
46
47
 
47
- def call(state, context: nil)
48
- # If no custom block provided, use default LLM behavior
49
- if @callable.nil? || @callable == method(:default_llm_call)
50
- default_llm_call(state, context)
48
+ def call(state, context: nil, observers: [])
49
+ # Auto-inject LLM config into the context for both default and custom blocks
50
+ merged_context = (context || {}).merge(
51
+ llm_client: @llm_client,
52
+ system_prompt: @system_prompt
53
+ )
54
+
55
+ begin
56
+ @llm_client&.set_observers(observers, @name) if observers.any?
57
+ rescue => e
58
+ raise NodeError, "Error setting observers for LLM client: #{e.message}"
59
+ end
60
+
61
+ # Delegate to Node's dispatcher so arity (0/1/2) is handled uniformly
62
+ case @callable.arity
63
+ when 0
64
+ @callable.call
65
+ when 1
66
+ @callable.call(state)
51
67
  else
52
- super(state, context: context)
68
+ @callable.call(state, merged_context)
53
69
  end
70
+ rescue => e
71
+ raise NodeError, "Error executing node '#{@name}': #{e.message}"
54
72
  end
55
73
 
56
74
  private
57
75
 
58
76
  def default_llm_call(state, context)
59
77
  messages = state[:messages] || []
60
- messages = [@system_prompt] + messages if @system_prompt && !messages.empty?
61
-
62
- response = @llm_client.call(messages)
63
-
78
+ if context && context[:system_prompt]
79
+ messages = [{ role: 'system', content: context[:system_prompt] }] + messages
80
+ end
81
+
82
+ response = (context[:llm_client] || @llm_client).call(messages)
83
+
64
84
  {
65
85
  messages: [{ role: 'assistant', content: response }],
66
86
  last_response: response
@@ -60,6 +60,16 @@ module LangGraphRB
60
60
  # Override in subclasses if cleanup needed
61
61
  end
62
62
 
63
+ # Called when LLM requests occur
64
+ def on_llm_request(event)
65
+ # Override in subclasses
66
+ end
67
+
68
+ # Called when LLM responses occur
69
+ def on_llm_response(event)
70
+ # Override in subclasses
71
+ end
72
+
63
73
  protected
64
74
 
65
75
  # Helper method to create standardized event structure
@@ -117,11 +127,11 @@ module LangGraphRB
117
127
 
118
128
  class NodeEvent
119
129
  attr_reader :type, :node_name, :node_class, :state_before, :state_after,
120
- :context, :thread_id, :step_number, :duration, :error, :result, :timestamp
130
+ :context, :thread_id, :step_number, :duration, :error, :result, :timestamp, :from_node
121
131
 
122
132
  def initialize(type:, node_name:, node_class: nil, state_before: nil, state_after: nil,
123
133
  context: nil, thread_id: nil, step_number: nil, duration: nil,
124
- error: nil, result: nil)
134
+ error: nil, result: nil, from_node: nil)
125
135
  @type = type
126
136
  @node_name = node_name
127
137
  @node_class = node_class
@@ -133,6 +143,7 @@ module LangGraphRB
133
143
  @duration = duration
134
144
  @error = error
135
145
  @result = result
146
+ @from_node = from_node
136
147
  @timestamp = Time.now.utc
137
148
  end
138
149
 
@@ -141,6 +152,7 @@ module LangGraphRB
141
152
  type: @type,
142
153
  node_name: @node_name,
143
154
  node_class: @node_class&.name,
155
+ from_node: @from_node,
144
156
  state_before: @state_before,
145
157
  state_after: @state_after,
146
158
  context: @context,
@@ -64,7 +64,7 @@ module LangGraphRB
64
64
  if dest_name == Graph::FINISH
65
65
  final_state = dest_state
66
66
  else
67
- next_active << ExecutionFrame.new(dest_name, dest_state, @step_number)
67
+ next_active << ExecutionFrame.new(dest_name, dest_state, @step_number, from_node: result[:node_name])
68
68
  end
69
69
  else
70
70
  # Use normal edge routing
@@ -78,7 +78,7 @@ module LangGraphRB
78
78
  if dest_name == Graph::FINISH
79
79
  final_state = dest_state
80
80
  else
81
- next_active << ExecutionFrame.new(dest_name, dest_state, @step_number)
81
+ next_active << ExecutionFrame.new(dest_name, dest_state, @step_number, from_node: result[:node_name])
82
82
  end
83
83
  end
84
84
  end
@@ -87,7 +87,7 @@ module LangGraphRB
87
87
  # Handle Send commands (map-reduce)
88
88
  result[:sends].each do |send_cmd|
89
89
  payload_state = result[:state].merge_delta(send_cmd.payload)
90
- next_active << ExecutionFrame.new(send_cmd.to, payload_state, @step_number)
90
+ next_active << ExecutionFrame.new(send_cmd.to, payload_state, @step_number, from_node: result[:node_name])
91
91
  end
92
92
 
93
93
  when :interrupt
@@ -96,7 +96,7 @@ module LangGraphRB
96
96
  user_input = @interrupt_handler.call(result[:interrupt])
97
97
  # Continue with user input merged into state
98
98
  updated_state = result[:state].merge_delta(user_input || {})
99
- next_active << ExecutionFrame.new(result[:node_name], updated_state, @step_number)
99
+ next_active << ExecutionFrame.new(result[:node_name], updated_state, @step_number, from_node: result[:node_name])
100
100
  else
101
101
  # No interrupt handler, treat as completion
102
102
  final_state = result[:state]
@@ -193,7 +193,7 @@ module LangGraphRB
193
193
  notify_observers(:on_graph_end, event)
194
194
  end
195
195
 
196
- def notify_node_start(node, state, context)
196
+ def notify_node_start(node, state, context, from_node: nil)
197
197
  event = Observers::NodeEvent.new(
198
198
  type: :start,
199
199
  node_name: node.name,
@@ -201,12 +201,13 @@ module LangGraphRB
201
201
  state_before: state,
202
202
  context: context,
203
203
  thread_id: @thread_id,
204
- step_number: @step_number
204
+ step_number: @step_number,
205
+ from_node: from_node
205
206
  )
206
207
  notify_observers(:on_node_start, event)
207
208
  end
208
209
 
209
- def notify_node_end(node, state_before, state_after, result, duration)
210
+ def notify_node_end(node, state_before, state_after, result, duration, from_node: nil)
210
211
  event = Observers::NodeEvent.new(
211
212
  type: :end,
212
213
  node_name: node.name,
@@ -216,12 +217,13 @@ module LangGraphRB
216
217
  result: result,
217
218
  duration: duration,
218
219
  thread_id: @thread_id,
219
- step_number: @step_number
220
+ step_number: @step_number,
221
+ from_node: from_node
220
222
  )
221
223
  notify_observers(:on_node_end, event)
222
224
  end
223
225
 
224
- def notify_node_error(node, state, error)
226
+ def notify_node_error(node, state, error, from_node: nil)
225
227
  event = Observers::NodeEvent.new(
226
228
  type: :error,
227
229
  node_name: node.name,
@@ -229,7 +231,8 @@ module LangGraphRB
229
231
  state_before: state,
230
232
  error: error,
231
233
  thread_id: @thread_id,
232
- step_number: @step_number
234
+ step_number: @step_number,
235
+ from_node: from_node
233
236
  )
234
237
  notify_observers(:on_node_error, event)
235
238
  end
@@ -251,7 +254,7 @@ module LangGraphRB
251
254
  # Execute each frame for this node
252
255
  executions.each do |frame|
253
256
  thread = Thread.new do
254
- execute_node_safely(node, frame.state, context, frame.step)
257
+ execute_node_safely(node, frame.state, context, frame.step, from_node: frame.from_node)
255
258
  end
256
259
  threads << thread
257
260
  end
@@ -267,12 +270,12 @@ module LangGraphRB
267
270
  end
268
271
 
269
272
  # Safely execute a single node
270
- def execute_node_safely(node, state, context, step)
271
- notify_node_start(node, state, context)
273
+ def execute_node_safely(node, state, context, step, from_node: nil)
274
+ notify_node_start(node, state, context, from_node: from_node)
272
275
 
273
276
  start_time = Time.now
274
277
  begin
275
- result = node.call(state, context: context)
278
+ result = node.call(state, context: context, observers: @observers)
276
279
  duration = Time.now - start_time
277
280
 
278
281
  processed_result = process_node_result(node.name, state, result, step)
@@ -285,11 +288,11 @@ module LangGraphRB
285
288
  state
286
289
  end
287
290
 
288
- notify_node_end(node, state, final_state, result, duration)
291
+ notify_node_end(node, state, final_state, result, duration, from_node: from_node)
289
292
  processed_result
290
293
  rescue => error
291
294
  duration = Time.now - start_time
292
- notify_node_error(node, state, error)
295
+ notify_node_error(node, state, error, from_node: from_node)
293
296
 
294
297
  {
295
298
  type: :error,
@@ -421,16 +424,17 @@ module LangGraphRB
421
424
 
422
425
  # Execution frame for tracking active node executions
423
426
  class ExecutionFrame
424
- attr_reader :node_name, :state, :step
427
+ attr_reader :node_name, :state, :step, :from_node
425
428
 
426
- def initialize(node_name, state, step)
429
+ def initialize(node_name, state, step, from_node: nil)
427
430
  @node_name = node_name.to_sym
428
431
  @state = state
429
432
  @step = step
433
+ @from_node = from_node
430
434
  end
431
435
 
432
436
  def to_s
433
- "#<ExecutionFrame node: #{@node_name}, step: #{@step}>"
437
+ "#<ExecutionFrame node: #{@node_name}, step: #{@step}, from: #{@from_node}>"
434
438
  end
435
439
  end
436
440
  end
@@ -1,3 +1,3 @@
1
1
  module LangGraphRB
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.3"
3
3
  end
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.1
4
+ version: 0.1.3
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-08-05 00:00:00.000000000 Z
11
+ date: 2025-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -106,11 +106,14 @@ extra_rdoc_files: []
106
106
  files:
107
107
  - ".gitignore"
108
108
  - Gemfile
109
+ - Gemfile.lock
109
110
  - README.md
110
111
  - SUMMARY.md
111
112
  - examples/advanced_example.rb
112
113
  - examples/basic_example.rb
113
114
  - examples/initial_state_example.rb
115
+ - examples/langfuse_example.rb
116
+ - examples/llmnode_example.rb
114
117
  - examples/observer_example.rb
115
118
  - examples/reducers_example.rb
116
119
  - examples/simple_test.rb