langgraph_rb 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.
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/langgraph_rb'
4
+
5
+ def simple_workflow_test
6
+ puts "=== Simple LangGraphRB Test ==="
7
+
8
+ # Create a simple linear workflow
9
+ graph = LangGraphRB::Graph.new do
10
+ node :start_process do |state|
11
+ puts "šŸš€ Starting process with input: #{state[:input]}"
12
+ {
13
+ step: 1,
14
+ message: "Process started",
15
+ processed_input: state[:input]&.upcase
16
+ }
17
+ end
18
+
19
+ node :analyze_data do |state|
20
+ puts "šŸ” Analyzing: #{state[:processed_input]}"
21
+ sleep(0.1) # Simulate work
22
+
23
+ analysis_result = case state[:processed_input]&.downcase
24
+ when /hello/
25
+ 'greeting_detected'
26
+ when /help/
27
+ 'help_request'
28
+ when /goodbye/
29
+ 'farewell_detected'
30
+ else
31
+ 'general_input'
32
+ end
33
+
34
+ {
35
+ step: 2,
36
+ analysis: analysis_result,
37
+ message: "Analysis complete: #{analysis_result}"
38
+ }
39
+ end
40
+
41
+ node :generate_response do |state|
42
+ puts "šŸ’­ Generating response for: #{state[:analysis]}"
43
+
44
+ response = case state[:analysis]
45
+ when 'greeting_detected'
46
+ "Hello! Nice to meet you!"
47
+ when 'help_request'
48
+ "I'm here to help! What do you need assistance with?"
49
+ when 'farewell_detected'
50
+ "Goodbye! Have a great day!"
51
+ else
52
+ "I understand. Tell me more about that."
53
+ end
54
+
55
+ {
56
+ step: 3,
57
+ final_response: response,
58
+ message: "Response generated",
59
+ completed: true
60
+ }
61
+ end
62
+
63
+ node :generate_response_2 do |state|
64
+ puts "šŸ’­ Generating response for: #{state[:analysis]}"
65
+ {
66
+ step: 4,
67
+ final_response: "Response generated",
68
+ message: "Response generated",
69
+ completed: true
70
+ }
71
+ end
72
+
73
+ # Define simple linear flow
74
+ set_entry_point :start_process
75
+ edge :start_process, :analyze_data
76
+ edge :analyze_data, :generate_response
77
+ edge :analyze_data, :generate_response_2
78
+ set_finish_point :generate_response
79
+ end
80
+
81
+ # Compile the graph
82
+ graph.compile!
83
+
84
+ puts "\nšŸ“Š Graph structure:"
85
+ puts graph.to_mermaid
86
+ puts
87
+
88
+ # Test with different inputs
89
+ test_inputs = [
90
+ "Hello there!",
91
+ "I need help",
92
+ "Goodbye everyone",
93
+ "Just a regular message"
94
+ ]
95
+
96
+ test_inputs.each_with_index do |input, i|
97
+ puts "\n--- Test #{i + 1}: '#{input}' ---"
98
+
99
+ result = graph.invoke({ input: input })
100
+
101
+ puts "āœ… Final response: #{result[:final_response]}"
102
+ puts " Steps completed: #{result[:step]}"
103
+ puts " Status: #{result[:completed] ? 'Complete' : 'Incomplete'}"
104
+ end
105
+
106
+ puts "\nāœ… Simple workflow test completed!"
107
+ end
108
+
109
+ def conditional_routing_test
110
+ puts "\n=== Conditional Routing Test ==="
111
+
112
+ graph = LangGraphRB::Graph.new do
113
+ node :router do |state|
114
+ input = state[:input]&.to_s&.downcase || ""
115
+ puts "šŸ”„ Routing input: #{input}"
116
+
117
+ {
118
+ input_type: case input
119
+ when /urgent/, /emergency/
120
+ 'urgent'
121
+ when /question/, /help/
122
+ 'question'
123
+ when /feedback/, /complaint/
124
+ 'feedback'
125
+ else
126
+ 'general'
127
+ end,
128
+ original_input: input
129
+ }
130
+ end
131
+
132
+ node :handle_urgent do |state|
133
+ puts "🚨 Handling urgent request"
134
+ {
135
+ response: "Your urgent request has been escalated to our priority team!",
136
+ priority: "high"
137
+ }
138
+ end
139
+
140
+ node :handle_question do |state|
141
+ puts "ā“ Handling question"
142
+ {
143
+ response: "Thank you for your question. Let me help you with that.",
144
+ priority: "medium"
145
+ }
146
+ end
147
+
148
+ node :handle_feedback do |state|
149
+ puts "šŸ’¬ Handling feedback"
150
+ {
151
+ response: "We appreciate your feedback and will review it carefully.",
152
+ priority: "medium"
153
+ }
154
+ end
155
+
156
+ node :handle_general do |state|
157
+ puts "šŸ“ Handling general request"
158
+ {
159
+ response: "Thank you for contacting us. We'll get back to you soon.",
160
+ priority: "normal"
161
+ }
162
+ end
163
+
164
+ # Set up routing
165
+ set_entry_point :router
166
+
167
+ conditional_edge :router, ->(state) { state[:input_type] }, {
168
+ 'urgent' => :handle_urgent,
169
+ 'question' => :handle_question,
170
+ 'feedback' => :handle_feedback,
171
+ 'general' => :handle_general
172
+ }
173
+
174
+ # All handlers go to finish
175
+ set_finish_point :handle_urgent
176
+ set_finish_point :handle_question
177
+ set_finish_point :handle_feedback
178
+ set_finish_point :handle_general
179
+ end
180
+
181
+ graph.compile!
182
+
183
+ puts "\nšŸ“Š Conditional routing graph:"
184
+ puts graph.to_mermaid
185
+ puts
186
+
187
+ # Test different routing scenarios
188
+ test_cases = [
189
+ "URGENT: System is down!",
190
+ "I have a question about your service",
191
+ "I want to give some feedback",
192
+ "Just saying hello"
193
+ ]
194
+
195
+ test_cases.each_with_index do |input, i|
196
+ puts "\n--- Routing Test #{i + 1}: '#{input}' ---"
197
+
198
+ result = graph.invoke({ input: input })
199
+
200
+ puts "šŸ“‹ Response: #{result[:response]}"
201
+ puts "šŸŽÆ Priority: #{result[:priority]}"
202
+ end
203
+
204
+ puts "\nāœ… Conditional routing test completed!"
205
+ end
206
+
207
+ def streaming_test
208
+ puts "\n=== Streaming Execution Test ==="
209
+
210
+ graph = LangGraphRB::Graph.new do
211
+ node :step_1 do |state|
212
+ puts " āš™ļø Step 1: Initialize"
213
+ sleep(0.3)
214
+ { step: 1, data: "initialized", progress: 25 }
215
+ end
216
+
217
+ node :step_2 do |state|
218
+ puts " āš™ļø Step 2: Process"
219
+ sleep(0.3)
220
+ { step: 2, data: state[:data] + " -> processed", progress: 50 }
221
+ end
222
+
223
+ node :step_3 do |state|
224
+ puts " āš™ļø Step 3: Validate"
225
+ sleep(0.3)
226
+ { step: 3, data: state[:data] + " -> validated", progress: 75 }
227
+ end
228
+
229
+ node :step_4 do |state|
230
+ puts " āš™ļø Step 4: Finalize"
231
+ sleep(0.3)
232
+ { step: 4, data: state[:data] + " -> finalized", progress: 100 }
233
+ end
234
+
235
+ set_entry_point :step_1
236
+ edge :step_1, :step_2
237
+ edge :step_2, :step_3
238
+ edge :step_3, :step_4
239
+ set_finish_point :step_4
240
+ end
241
+
242
+ graph.compile!
243
+
244
+ puts "šŸ“” Streaming execution progress:"
245
+ start_time = Time.now
246
+
247
+ result = graph.stream({ input: "test_data" }) do |step_result|
248
+ elapsed = (Time.now - start_time).round(2)
249
+ puts " šŸ“Š [#{elapsed}s] Step #{step_result[:step]}: #{step_result[:active_nodes]}"
250
+ if step_result[:state][:progress]
251
+ puts " Progress: #{step_result[:state][:progress]}%"
252
+ end
253
+ end
254
+
255
+ puts "\nšŸ Final result: #{result[:state][:data]}"
256
+ puts "ā±ļø Total time: #{(Time.now - start_time).round(2)}s"
257
+
258
+ puts "\nāœ… Streaming test completed!"
259
+ end
260
+
261
+ # Run tests
262
+ if __FILE__ == $0
263
+ #simple_workflow_test
264
+ conditional_routing_test
265
+ #streaming_test
266
+ end
@@ -0,0 +1,43 @@
1
+ require_relative 'lib/langgraph_rb/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "langgraph_rb"
5
+ spec.version = LangGraphRB::VERSION
6
+ spec.authors = ["Julian Toro"]
7
+ spec.email = ["fulit103@gmail.com"]
8
+
9
+ spec.summary = "A Ruby library for building stateful, multi-actor applications with directed graphs"
10
+ spec.description = <<~DESC
11
+ LangGraphRB is a Ruby library inspired by LangGraph for building stateful, multi-actor applications
12
+ using directed graphs. It provides a framework for orchestrating complex workflows with support for
13
+ parallel execution, checkpointing, human-in-the-loop interactions, and map-reduce operations.
14
+ DESC
15
+ spec.homepage = "https://github.com/fulit103/langgraph_rb"
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = ">= 3.0.0"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = spec.homepage
21
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ f.match(%r{\A(?:test|spec|features)/})
27
+ end
28
+ end
29
+
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ # Runtime dependencies
35
+ spec.add_dependency "json", "~> 2.0"
36
+
37
+ # Development dependencies
38
+ spec.add_development_dependency "bundler", "~> 2.0"
39
+ spec.add_development_dependency "rake", "~> 13.0"
40
+ spec.add_development_dependency "rspec", "~> 3.0"
41
+ spec.add_development_dependency "pry", "~> 0.14"
42
+ spec.add_development_dependency "rubocop", "~> 1.0"
43
+ end
@@ -0,0 +1,132 @@
1
+ module LangGraphRB
2
+ # Command combines a state update with a routing decision
3
+ class Command
4
+ attr_reader :update, :goto
5
+
6
+ def initialize(update: nil, goto: nil)
7
+ @update = update || {}
8
+ @goto = goto
9
+ end
10
+
11
+ def self.update(state_delta)
12
+ new(update: state_delta)
13
+ end
14
+
15
+ def self.goto(destination)
16
+ new(goto: destination)
17
+ end
18
+
19
+ def self.update_and_goto(state_delta, destination)
20
+ new(update: state_delta, goto: destination)
21
+ end
22
+
23
+ def to_s
24
+ parts = []
25
+ parts << "update: #{@update.inspect}" unless @update.empty?
26
+ parts << "goto: #{@goto}" if @goto
27
+ "#<Command #{parts.join(', ')}>"
28
+ end
29
+
30
+ def inspect
31
+ to_s
32
+ end
33
+ end
34
+
35
+ # Send creates a new parallel execution branch with specific payload
36
+ class Send
37
+ attr_reader :to, :payload
38
+
39
+ def initialize(to:, payload: {})
40
+ @to = to.to_sym
41
+ @payload = payload || {}
42
+ end
43
+
44
+ def to_s
45
+ "#<Send to: #{@to}, payload: #{@payload.inspect}>"
46
+ end
47
+
48
+ def inspect
49
+ to_s
50
+ end
51
+ end
52
+
53
+ # MultiSend creates multiple parallel execution branches
54
+ class MultiSend
55
+ attr_reader :sends
56
+
57
+ def initialize(*sends)
58
+ @sends = sends.flatten
59
+ end
60
+
61
+ def self.to_multiple(destinations, payload = {})
62
+ sends = destinations.map { |dest| Send.new(to: dest, payload: payload) }
63
+ new(sends)
64
+ end
65
+
66
+ def self.fan_out(node, payloads)
67
+ sends = payloads.map { |payload| Send.new(to: node, payload: payload) }
68
+ new(sends)
69
+ end
70
+
71
+ def to_s
72
+ "#<MultiSend #{@sends.map(&:to_s).join(', ')}>"
73
+ end
74
+
75
+ def inspect
76
+ to_s
77
+ end
78
+ end
79
+
80
+ # Interrupt execution and wait for human input
81
+ class Interrupt
82
+ attr_reader :message, :data
83
+
84
+ def initialize(message: "Human input required", data: {})
85
+ @message = message
86
+ @data = data
87
+ end
88
+
89
+ def to_s
90
+ "#<Interrupt: #{@message}>"
91
+ end
92
+
93
+ def inspect
94
+ to_s
95
+ end
96
+ end
97
+
98
+ # Helper module for creating commands
99
+ module Commands
100
+ def self.update(state_delta)
101
+ Command.update(state_delta)
102
+ end
103
+
104
+ def self.goto(destination)
105
+ Command.goto(destination)
106
+ end
107
+
108
+ def self.update_and_goto(state_delta, destination)
109
+ Command.update_and_goto(state_delta, destination)
110
+ end
111
+
112
+ def self.send_to(destination, payload = {})
113
+ Send.new(to: destination, payload: payload)
114
+ end
115
+
116
+ def self.send_to_multiple(destinations, payload = {})
117
+ MultiSend.to_multiple(destinations, payload)
118
+ end
119
+
120
+ def self.fan_out(node, payloads)
121
+ MultiSend.fan_out(node, payloads)
122
+ end
123
+
124
+ def self.interrupt(message: "Human input required", data: {})
125
+ Interrupt.new(message: message, data: data)
126
+ end
127
+
128
+ def self.end_execution(final_state = {})
129
+ Command.update_and_goto(final_state, Graph::FINISH)
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,141 @@
1
+ module LangGraphRB
2
+ # Simple edge connecting two nodes
3
+ class Edge
4
+ attr_reader :from, :to
5
+
6
+ def initialize(from, to)
7
+ @from = from.to_sym
8
+ @to = to.to_sym
9
+ end
10
+
11
+ def route(state, context: nil)
12
+ [@to]
13
+ end
14
+
15
+ def to_s
16
+ "#{@from} -> #{@to}"
17
+ end
18
+
19
+ def inspect
20
+ "#<Edge: #{to_s}>"
21
+ end
22
+
23
+ def ==(other)
24
+ other.is_a?(Edge) && @from == other.from && @to == other.to
25
+ end
26
+ end
27
+
28
+ # Conditional edge that uses a router function to determine destination(s)
29
+ class ConditionalEdge
30
+ attr_reader :from, :router, :path_map
31
+
32
+ def initialize(from, router, path_map = nil)
33
+ @from = from.to_sym
34
+ @router = router
35
+ @path_map = path_map || {}
36
+ end
37
+
38
+ # Route based on the router function result
39
+ def route(state, context: nil)
40
+ result = case @router.arity
41
+ when 0
42
+ @router.call
43
+ when 1
44
+ @router.call(state)
45
+ else
46
+ @router.call(state, context)
47
+ end
48
+
49
+ # Convert result to destinations
50
+ destinations = case result
51
+ when Array
52
+ result
53
+ when String, Symbol
54
+ [result]
55
+ when Hash
56
+ # Support for multiple destinations with different states
57
+ result.keys
58
+ else
59
+ [result]
60
+ end
61
+
62
+ # Map through path_map if provided
63
+ destinations.map do |dest|
64
+ mapped = @path_map[dest.to_s] || @path_map[dest.to_sym] || dest
65
+ mapped.to_sym
66
+ end
67
+ end
68
+
69
+ def to_s
70
+ "#{@from} -> [conditional]"
71
+ end
72
+
73
+ def inspect
74
+ "#<ConditionalEdge: #{to_s}>"
75
+ end
76
+ end
77
+
78
+ # Fan-out edge that creates multiple parallel executions
79
+ class FanOutEdge
80
+ attr_reader :from, :destinations
81
+
82
+ def initialize(from, destinations)
83
+ @from = from.to_sym
84
+ @destinations = destinations.map(&:to_sym)
85
+ end
86
+
87
+ def route(state, context: nil)
88
+ @destinations
89
+ end
90
+
91
+ def to_s
92
+ "#{@from} -> #{@destinations.inspect}"
93
+ end
94
+
95
+ def inspect
96
+ "#<FanOutEdge: #{to_s}>"
97
+ end
98
+ end
99
+
100
+ # Helper class for building conditional routing
101
+ class Router
102
+ def self.build(&block)
103
+ new.tap { |r| r.instance_eval(&block) }
104
+ end
105
+
106
+ def initialize
107
+ @conditions = []
108
+ end
109
+
110
+ def when(condition, destination)
111
+ @conditions << [condition, destination]
112
+ self
113
+ end
114
+
115
+ def otherwise(destination)
116
+ @default = destination
117
+ self
118
+ end
119
+
120
+ def call(state, context = nil)
121
+ @conditions.each do |condition, destination|
122
+ result = case condition.arity
123
+ when 0
124
+ condition.call
125
+ when 1
126
+ condition.call(state)
127
+ else
128
+ condition.call(state, context)
129
+ end
130
+
131
+ return destination if result
132
+ end
133
+
134
+ @default || raise("No matching condition and no default specified")
135
+ end
136
+
137
+ def to_proc
138
+ method(:call).to_proc
139
+ end
140
+ end
141
+ end