langgraphrb_rails 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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +816 -0
  3. data/Rakefile +23 -0
  4. data/app/assets/javascripts/langgraphrb_rails.js +153 -0
  5. data/app/assets/stylesheets/langgraphrb_rails.css +95 -0
  6. data/lib/generators/langgraph_rb/compatibility.rb +71 -0
  7. data/lib/generators/langgraph_rb/controller/templates/controller.rb +54 -0
  8. data/lib/generators/langgraph_rb/controller/templates/view.html.erb +101 -0
  9. data/lib/generators/langgraph_rb/controller_generator.rb +39 -0
  10. data/lib/generators/langgraph_rb/graph/templates/graph.rb +68 -0
  11. data/lib/generators/langgraph_rb/graph_generator.rb +23 -0
  12. data/lib/generators/langgraph_rb/install/templates/README +45 -0
  13. data/lib/generators/langgraph_rb/install/templates/example_graph.rb +89 -0
  14. data/lib/generators/langgraph_rb/install/templates/initializer.rb +35 -0
  15. data/lib/generators/langgraph_rb/install/templates/langgraph_rb.yml +45 -0
  16. data/lib/generators/langgraph_rb/install_generator.rb +34 -0
  17. data/lib/generators/langgraph_rb/job/templates/job.rb +38 -0
  18. data/lib/generators/langgraph_rb/job_generator.rb +27 -0
  19. data/lib/generators/langgraph_rb/model/templates/migration.rb +12 -0
  20. data/lib/generators/langgraph_rb/model/templates/model.rb +15 -0
  21. data/lib/generators/langgraph_rb/model_generator.rb +34 -0
  22. data/lib/generators/langgraph_rb/task/templates/task.rake +58 -0
  23. data/lib/generators/langgraph_rb/task_generator.rb +23 -0
  24. data/lib/generators/langgraphrb_rails/compatibility.rb +71 -0
  25. data/lib/generators/langgraphrb_rails/controller/templates/controller.rb +30 -0
  26. data/lib/generators/langgraphrb_rails/controller/templates/view.html.erb +112 -0
  27. data/lib/generators/langgraphrb_rails/controller_generator.rb +29 -0
  28. data/lib/generators/langgraphrb_rails/graph/templates/graph.rb +14 -0
  29. data/lib/generators/langgraphrb_rails/graph/templates/node.rb +16 -0
  30. data/lib/generators/langgraphrb_rails/graph_generator.rb +48 -0
  31. data/lib/generators/langgraphrb_rails/install/templates/config.yml +30 -0
  32. data/lib/generators/langgraphrb_rails/install/templates/example_graph.rb +44 -0
  33. data/lib/generators/langgraphrb_rails/install/templates/initializer.rb +27 -0
  34. data/lib/generators/langgraphrb_rails/install_generator.rb +35 -0
  35. data/lib/generators/langgraphrb_rails/jobs/templates/run_job.rb +45 -0
  36. data/lib/generators/langgraphrb_rails/jobs_generator.rb +34 -0
  37. data/lib/generators/langgraphrb_rails/model/templates/migration.rb +20 -0
  38. data/lib/generators/langgraphrb_rails/model/templates/model.rb +14 -0
  39. data/lib/generators/langgraphrb_rails/model_generator.rb +30 -0
  40. data/lib/generators/langgraphrb_rails/persistence/templates/create_langgraph_runs.rb +18 -0
  41. data/lib/generators/langgraphrb_rails/persistence/templates/langgraph_run.rb +56 -0
  42. data/lib/generators/langgraphrb_rails/persistence_generator.rb +28 -0
  43. data/lib/generators/langgraphrb_rails/task/templates/task.rake +30 -0
  44. data/lib/generators/langgraphrb_rails/task_generator.rb +17 -0
  45. data/lib/generators/langgraphrb_rails/tracing/templates/traced.rb +45 -0
  46. data/lib/generators/langgraphrb_rails/tracing_generator.rb +63 -0
  47. data/lib/langgraphrb_rails/configuration.rb +47 -0
  48. data/lib/langgraphrb_rails/engine.rb +20 -0
  49. data/lib/langgraphrb_rails/helper.rb +141 -0
  50. data/lib/langgraphrb_rails/middleware/streaming.rb +77 -0
  51. data/lib/langgraphrb_rails/railtie.rb +55 -0
  52. data/lib/langgraphrb_rails/stores/active_record.rb +51 -0
  53. data/lib/langgraphrb_rails/stores/redis.rb +57 -0
  54. data/lib/langgraphrb_rails/test_helper.rb +126 -0
  55. data/lib/langgraphrb_rails/version.rb +28 -0
  56. data/lib/langgraphrb_rails.rb +111 -0
  57. data/lib/tasks/langgraphrb_rails_tasks.rake +62 -0
  58. metadata +217 -0
@@ -0,0 +1,141 @@
1
+ module LanggraphrbRails
2
+ module Helper
3
+ # Helper method to render a chat interface for a LangGraphRB graph
4
+ def langgraph_chat_interface(options = {})
5
+ graph_class = options[:graph_class]
6
+ container_id = options[:container_id] || 'langgraph-chat'
7
+ placeholder = options[:placeholder] || 'Type your message here...'
8
+ submit_text = options[:submit_text] || 'Send'
9
+ submit_path = options[:submit_path] || url_for(controller: controller_name, action: 'create')
10
+ css_class = ['langgraph-chat-container', options[:css_class]].compact.join(' ')
11
+
12
+ content_tag :div, class: css_class, id: container_id do
13
+ messages_div = content_tag(:div, class: 'langgraph-messages', id: "#{container_id}-messages") do
14
+ # Render existing messages if provided
15
+ if options[:messages].present?
16
+ safe_join(options[:messages].map do |message|
17
+ content_tag(:div, class: "langgraph-message #{message[:role]}") do
18
+ content_tag(:p, message[:content])
19
+ end
20
+ end)
21
+ else
22
+ ""
23
+ end
24
+ end
25
+
26
+ form = form_with(url: submit_path, class: 'langgraph-chat-form', id: "#{container_id}-form", data: { turbo: false }) do |f|
27
+ f.text_area(:message, class: 'langgraph-chat-input', placeholder: placeholder) +
28
+ f.submit(submit_text, class: 'langgraph-chat-submit')
29
+ end
30
+
31
+ messages_div + form
32
+ end
33
+ end
34
+
35
+ # Helper method to render a streaming chat interface
36
+ def langgraph_streaming_chat_interface(options = {})
37
+ container_id = options[:container_id] || 'langgraph-streaming-chat'
38
+ placeholder = options[:placeholder] || 'Type your message here...'
39
+ submit_text = options[:submit_text] || 'Send'
40
+ stream_path = options[:stream_path] || url_for(controller: controller_name, action: 'stream')
41
+ css_class = ['langgraph-chat-container', options[:css_class]].compact.join(' ')
42
+
43
+ content_tag :div, class: css_class, id: container_id do
44
+ messages_div = content_tag(:div, class: 'langgraph-messages', id: "#{container_id}-messages") do
45
+ # Render existing messages if provided
46
+ if options[:messages].present?
47
+ safe_join(options[:messages].map do |message|
48
+ content_tag(:div, class: "langgraph-message #{message[:role]}") do
49
+ content_tag(:p, message[:content])
50
+ end
51
+ end)
52
+ else
53
+ ""
54
+ end
55
+ end
56
+
57
+ form = form_with(url: stream_path, method: :post, class: 'langgraph-chat-form', id: "#{container_id}-form", data: { turbo: false }) do |f|
58
+ f.text_area(:message, class: 'langgraph-chat-input', placeholder: placeholder) +
59
+ f.submit(submit_text, class: 'langgraph-chat-submit') +
60
+ content_tag(:div, '', class: 'langgraph-typing-indicator', id: "#{container_id}-typing")
61
+ end
62
+
63
+ # Add JavaScript for streaming
64
+ js_code = javascript_tag do
65
+ <<-JS.html_safe
66
+ document.addEventListener('DOMContentLoaded', function() {
67
+ const form = document.getElementById('#{container_id}-form');
68
+ const messagesContainer = document.getElementById('#{container_id}-messages');
69
+ const typingIndicator = document.getElementById('#{container_id}-typing');
70
+
71
+ form.addEventListener('submit', function(e) {
72
+ e.preventDefault();
73
+
74
+ const messageInput = form.querySelector('textarea[name="message"]');
75
+ const userMessage = messageInput.value.trim();
76
+
77
+ if (!userMessage) return;
78
+
79
+ // Add user message to the UI
80
+ const userDiv = document.createElement('div');
81
+ userDiv.className = 'langgraph-message user';
82
+ userDiv.innerHTML = '<p>' + userMessage.replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</p>';
83
+ messagesContainer.appendChild(userDiv);
84
+
85
+ // Show typing indicator
86
+ typingIndicator.style.display = 'block';
87
+
88
+ // Clear the input
89
+ messageInput.value = '';
90
+
91
+ // Scroll to bottom
92
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
93
+
94
+ // Start the EventSource connection
95
+ const eventSource = new EventSource(`#{stream_path}?message=${encodeURIComponent(userMessage)}`);
96
+
97
+ // Create a div for the assistant's response
98
+ const assistantDiv = document.createElement('div');
99
+ assistantDiv.className = 'langgraph-message assistant';
100
+ const assistantP = document.createElement('p');
101
+ assistantDiv.appendChild(assistantP);
102
+ messagesContainer.appendChild(assistantDiv);
103
+
104
+ // Handle incoming messages
105
+ eventSource.onmessage = function(event) {
106
+ try {
107
+ const data = JSON.parse(event.data);
108
+
109
+ // Hide typing indicator when complete
110
+ if (data.completed) {
111
+ typingIndicator.style.display = 'none';
112
+ eventSource.close();
113
+ }
114
+
115
+ // Update the assistant's message if there's a response
116
+ if (data.state && data.state.response) {
117
+ assistantP.textContent = data.state.response;
118
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
119
+ }
120
+ } catch (e) {
121
+ console.error('Error parsing event data:', e);
122
+ typingIndicator.style.display = 'none';
123
+ eventSource.close();
124
+ }
125
+ };
126
+
127
+ // Handle errors
128
+ eventSource.onerror = function() {
129
+ typingIndicator.style.display = 'none';
130
+ eventSource.close();
131
+ };
132
+ });
133
+ });
134
+ JS
135
+ end
136
+
137
+ messages_div + form + js_code
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,77 @@
1
+ module LanggraphrbRails
2
+ module Middleware
3
+ class Streaming
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ # Check if this is a LangGraphRB streaming request
10
+ if env["PATH_INFO"].match?(/\/langgraph_rb\/stream/)
11
+ handle_streaming(env)
12
+ else
13
+ @app.call(env)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def handle_streaming(env)
20
+ request = Rack::Request.new(env)
21
+
22
+ # Extract parameters
23
+ graph_class_name = request.params["graph_class"]
24
+ input = request.params["input"]
25
+ thread_id = request.params["thread_id"]
26
+
27
+ # Validate parameters
28
+ unless graph_class_name && input
29
+ return [400, {"Content-Type" => "text/plain"}, ["Missing required parameters"]]
30
+ end
31
+
32
+ # Find the graph class
33
+ begin
34
+ graph_class = graph_class_name.constantize
35
+ rescue NameError
36
+ return [404, {"Content-Type" => "text/plain"}, ["Graph class not found"]]
37
+ end
38
+
39
+ # Set up streaming response
40
+ [200, {
41
+ "Content-Type" => "text/event-stream",
42
+ "Cache-Control" => "no-cache",
43
+ "Connection" => "keep-alive"
44
+ }, StreamingBody.new(graph_class, input, thread_id)]
45
+ end
46
+
47
+ class StreamingBody
48
+ def initialize(graph_class, input, thread_id = nil)
49
+ @graph_class = graph_class
50
+ @input = input
51
+ @thread_id = thread_id || SecureRandom.hex(8)
52
+ end
53
+
54
+ def each
55
+ # Create a store for persistence
56
+ store = LanggraphrbRails.create_store
57
+
58
+ # Stream the graph execution
59
+ @graph_class.stream(
60
+ { input: @input },
61
+ store: store,
62
+ thread_id: @thread_id
63
+ ) do |step_result|
64
+ # Format as server-sent event
65
+ yield "data: #{step_result.to_json}\n\n"
66
+
67
+ # Close the connection when completed
68
+ break if step_result[:completed]
69
+ end
70
+ rescue => e
71
+ # Send error as event
72
+ yield "data: #{{ error: e.message }.to_json}\n\n"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,55 @@
1
+ require 'rails'
2
+ require 'langgraphrb_rails/helper'
3
+ require 'yaml'
4
+
5
+ module LanggraphrbRails
6
+ class Railtie < Rails::Railtie
7
+ initializer "langgraphrb_rails.configure_rails_initialization" do
8
+ # Load configuration from config/langgraph_rb.yml if it exists
9
+ config_file = Rails.root.join('config', 'langgraph_rb.yml')
10
+ if File.exist?(config_file)
11
+ # Use safe_load for better security and compatibility with newer Ruby/Rails
12
+ config = YAML.safe_load(ERB.new(File.read(config_file)).result, aliases: true)[Rails.env]
13
+ LanggraphrbRails.configure do |c|
14
+ config&.each do |key, value|
15
+ c.send("#{key}=", value) if c.respond_to?("#{key}=")
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ # Include helpers in views - compatible with all Rails versions
22
+ initializer "langgraphrb_rails.view_helpers" do
23
+ ActiveSupport.on_load(:action_view) do
24
+ include LanggraphrbRails::Helper
25
+ end
26
+ end
27
+
28
+ # Add asset paths with compatibility for Rails 7.1+ and 8.x
29
+ initializer "langgraphrb_rails.assets" do |app|
30
+ if app.config.respond_to?(:assets)
31
+ stylesheets_path = File.expand_path("../../app/assets/stylesheets", __dir__)
32
+ javascripts_path = File.expand_path("../../app/assets/javascripts", __dir__)
33
+
34
+ # Add paths to assets if the assets configuration exists
35
+ if app.config.assets.respond_to?(:paths)
36
+ app.config.assets.paths << stylesheets_path
37
+ app.config.assets.paths << javascripts_path
38
+ end
39
+
40
+ # Handle precompilation for different Rails versions
41
+ if app.config.assets.respond_to?(:precompile)
42
+ app.config.assets.precompile += %w(langgraphrb_rails.css langgraphrb_rails.js)
43
+ end
44
+ end
45
+ end
46
+
47
+ # Add rake tasks with better error handling
48
+ rake_tasks do
49
+ rake_file = File.expand_path("../../tasks/langgraphrb_rails_tasks.rake", __dir__)
50
+ if File.exist?(rake_file)
51
+ load rake_file
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,51 @@
1
+ module LanggraphrbRails
2
+ module Stores
3
+ class ActiveRecordStore
4
+ def initialize(options = {})
5
+ @model = options[:model] || raise(ArgumentError, "Model class is required")
6
+ @ttl = options[:ttl] # Optional TTL for cleanup
7
+ end
8
+
9
+ def get(thread_id)
10
+ record = @model.find_by(thread_id: thread_id)
11
+ return nil unless record
12
+
13
+ record.state
14
+ end
15
+
16
+ def put(thread_id, data)
17
+ record = @model.find_or_initialize_by(thread_id: thread_id)
18
+ record.state = data
19
+ record.save!
20
+
21
+ # Set expiration if TTL is provided
22
+ if @ttl && defined?(Rails.cache)
23
+ Rails.cache.write("#{@model.name.underscore}:#{thread_id}:expiry", true, expires_in: @ttl)
24
+ end
25
+
26
+ data
27
+ end
28
+
29
+ def delete(thread_id)
30
+ record = @model.find_by(thread_id: thread_id)
31
+ record&.destroy
32
+ end
33
+
34
+ def exists?(thread_id)
35
+ @model.exists?(thread_id: thread_id)
36
+ end
37
+
38
+ # Cleanup expired records (can be called from a background job)
39
+ def cleanup_expired
40
+ return unless @ttl && defined?(Rails.cache)
41
+
42
+ @model.find_each do |record|
43
+ # Handle both String and Class names
44
+ model_name = @model.name.is_a?(String) ? @model.name.downcase : @model.name.underscore
45
+ cache_key = "#{model_name}:#{record.thread_id}:expiry"
46
+ record.destroy unless Rails.cache.exist?(cache_key)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,57 @@
1
+ require 'redis'
2
+ require 'json'
3
+
4
+ module LanggraphrbRails
5
+ module Stores
6
+ class RedisStore
7
+ # Add namespace accessor
8
+ attr_reader :namespace
9
+
10
+ # Class method to create a new store
11
+ def self.create(options = {})
12
+ new(options)
13
+ end
14
+
15
+ def initialize(options = {})
16
+ @redis = options[:client] || Redis.new(url: options[:url] || ENV['REDIS_URL'] || 'redis://localhost:6379/0')
17
+ @namespace = options[:namespace] || 'langgraph_rb'
18
+ @ttl = options[:ttl] || 86400 # 24 hours default TTL
19
+ end
20
+
21
+ def get(thread_id)
22
+ key = namespaced_key(thread_id)
23
+ data = @redis.get(key)
24
+ return nil unless data
25
+
26
+ JSON.parse(data)
27
+ end
28
+
29
+ def put(thread_id, data, ttl = nil)
30
+ key = namespaced_key(thread_id)
31
+ if ttl
32
+ @redis.set(key, JSON.generate(data), ex: ttl)
33
+ else
34
+ @redis.set(key, JSON.generate(data))
35
+ @redis.expire(key, @ttl) if @ttl
36
+ end
37
+ data
38
+ end
39
+
40
+ def delete(thread_id)
41
+ key = namespaced_key(thread_id)
42
+ @redis.del(key)
43
+ end
44
+
45
+ def exists?(thread_id)
46
+ key = namespaced_key(thread_id)
47
+ @redis.exists?(key)
48
+ end
49
+
50
+ private
51
+
52
+ def namespaced_key(thread_id)
53
+ "#{@namespace}:#{thread_id}"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,126 @@
1
+ require 'langgraph_rb'
2
+
3
+ module LanggraphrbRails
4
+ # Test helper module for LanggraphRB Rails integration
5
+ module TestHelper
6
+ # Create a memory store for testing
7
+ def with_memory_store
8
+ original_store = LanggraphrbRails.configuration.store_adapter
9
+ original_options = LanggraphrbRails.configuration.store_options
10
+
11
+ begin
12
+ # Set memory store for testing
13
+ LanggraphrbRails.configure_store do |config|
14
+ config.adapter = :memory
15
+ config.options = {}
16
+ end
17
+
18
+ # Execute the block with memory store
19
+ yield
20
+ ensure
21
+ # Restore original store configuration
22
+ LanggraphrbRails.configure_store do |config|
23
+ config.adapter = original_store
24
+ config.options = original_options
25
+ end
26
+ end
27
+ end
28
+
29
+ # Create a test store with the specified adapter
30
+ def with_store(adapter, options = {})
31
+ original_store = LanggraphrbRails.configuration.store_adapter
32
+ original_options = LanggraphrbRails.configuration.store_options
33
+
34
+ begin
35
+ # Set specified store for testing
36
+ LanggraphrbRails.configure_store do |config|
37
+ config.adapter = adapter
38
+ config.options = options
39
+ end
40
+
41
+ # Execute the block with the specified store
42
+ yield
43
+ ensure
44
+ # Restore original store configuration
45
+ LanggraphrbRails.configure_store do |config|
46
+ config.adapter = original_store
47
+ config.options = original_options
48
+ end
49
+ end
50
+ end
51
+
52
+ # Mock a graph for testing
53
+ def mock_graph(nodes = {}, edges = {}, entry_point = nil)
54
+ Class.new do
55
+ include LangGraphRB::Graph
56
+
57
+ define_singleton_method(:define_nodes) do
58
+ nodes
59
+ end
60
+
61
+ define_singleton_method(:define_edges) do
62
+ edges
63
+ end
64
+
65
+ define_singleton_method(:build) do
66
+ LangGraphRB::Graph.build(
67
+ nodes: define_nodes,
68
+ edges: define_edges,
69
+ entry_point: entry_point || nodes.keys.first
70
+ )
71
+ end
72
+
73
+ define_singleton_method(:instance) do
74
+ @instance ||= build
75
+ end
76
+
77
+ define_singleton_method(:invoke) do |input, options = {}|
78
+ state = { input: input }
79
+ context = options[:context] || {}
80
+ store = options[:store] || LanggraphrbRails.create_store
81
+ thread_id = options[:thread_id] || SecureRandom.hex(8)
82
+
83
+ result = instance.invoke(
84
+ state,
85
+ context: context,
86
+ store: store,
87
+ thread_id: thread_id
88
+ )
89
+
90
+ result.merge(thread_id: thread_id)
91
+ end
92
+
93
+ define_singleton_method(:stream) do |input, options = {}, &block|
94
+ state = { input: input }
95
+ context = options[:context] || {}
96
+ store = options[:store] || LanggraphrbRails.create_store
97
+ thread_id = options[:thread_id] || SecureRandom.hex(8)
98
+
99
+ instance.stream(
100
+ state,
101
+ context: context,
102
+ store: store,
103
+ thread_id: thread_id
104
+ ) do |step|
105
+ block.call(step.merge(thread_id: thread_id))
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ # Create a test graph with a simple echo node
112
+ def create_echo_graph
113
+ mock_graph(
114
+ {
115
+ echo: ->(state, _context) {
116
+ state.merge(response: "Echo: #{state[:input]}")
117
+ }
118
+ },
119
+ {
120
+ echo: :END
121
+ },
122
+ :echo
123
+ )
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,28 @@
1
+ module LanggraphrbRails
2
+ # Current version of the gem
3
+ VERSION = "0.1.0"
4
+
5
+ # Minimum required version of langgraph_rb
6
+ LANGGRAPH_RB_MIN_VERSION = "0.1.2"
7
+
8
+ # Minimum required version of Rails
9
+ RAILS_MIN_VERSION = "6.0.0"
10
+
11
+ # Maximum supported version of Rails
12
+ RAILS_MAX_VERSION = "8.0.2"
13
+
14
+ # Version history
15
+ VERSION_HISTORY = {
16
+ "0.1.0" => "Initial release with generators, helpers, stores, and middleware. Improved compatibility with Rails 7.1.x and 8.x."
17
+ }
18
+
19
+ # Returns the version information as a hash
20
+ def self.version_info
21
+ {
22
+ version: VERSION,
23
+ langgraph_rb_min_version: LANGGRAPH_RB_MIN_VERSION,
24
+ rails_min_version: RAILS_MIN_VERSION,
25
+ rails_max_version: RAILS_MAX_VERSION
26
+ }
27
+ end
28
+ end
@@ -0,0 +1,111 @@
1
+ require "langgraphrb_rails/version"
2
+ require "langgraphrb_rails/configuration"
3
+ require "langgraphrb_rails/helper"
4
+ require "langgraphrb_rails/engine"
5
+ require "langgraphrb_rails/railtie" if defined?(Rails)
6
+ require "langgraph_rb"
7
+
8
+ module LanggraphrbRails
9
+ class << self
10
+ attr_writer :configuration
11
+
12
+ # Get the current configuration
13
+ def configuration
14
+ @configuration ||= Configuration.new
15
+ end
16
+
17
+ # Configure the gem
18
+ def configure
19
+ yield(configuration)
20
+ end
21
+
22
+ # Configure from a hash (e.g., from YAML)
23
+ def configure_from_hash(config_hash)
24
+ return unless config_hash.is_a?(Hash)
25
+
26
+ if config_hash['store'].is_a?(Hash)
27
+ configuration.store_adapter = config_hash['store']['adapter'] if config_hash['store']['adapter']
28
+ configuration.store_options = config_hash['store']['options'] if config_hash['store']['options']
29
+ end
30
+
31
+ if config_hash['job'].is_a?(Hash)
32
+ configuration.job_queue = config_hash['job']['queue'] if config_hash['job']['queue']
33
+ configuration.max_retries = config_hash['job']['max_retries'] if config_hash['job']['max_retries']
34
+ end
35
+
36
+ if config_hash['error'].is_a?(Hash)
37
+ configuration.on_error = config_hash['error']['policy'] if config_hash['error']['policy']
38
+ configuration.max_retries = config_hash['error']['max_retries'] if config_hash['error']['max_retries']
39
+ end
40
+ end
41
+
42
+ # Start a new graph run
43
+ def start!(graph:, context: {}, state: {})
44
+ # Generate a unique thread ID
45
+ thread_id = SecureRandom.uuid
46
+
47
+ # Create the run record if we have persistence
48
+ if defined?(LanggraphRun)
49
+ graph_name = graph.is_a?(String) ? graph : graph.name
50
+ run = LanggraphRun.create!(
51
+ thread_id: thread_id,
52
+ graph: graph_name,
53
+ context: context,
54
+ state: state,
55
+ status: :queued
56
+ )
57
+
58
+ return run
59
+ else
60
+ # If no persistence is available, delegate to LangGraphRB
61
+ store = create_store
62
+ LangGraphRB::Runner.start!(graph: graph, context: context, state: state, store: store)
63
+ return { thread_id: thread_id }
64
+ end
65
+ end
66
+
67
+ # Resume a graph run
68
+ def resume!(run_id)
69
+ # Find the run record
70
+ run = LanggraphRun.find(run_id)
71
+
72
+ # Create a store with the run's thread_id
73
+ store = create_store
74
+
75
+ # Get the graph class
76
+ graph_class = run.graph.constantize
77
+
78
+ # Resume execution
79
+ result = LangGraphRB::Runner.resume!(
80
+ thread_id: run.thread_id,
81
+ store: store
82
+ )
83
+
84
+ # Update the run record
85
+ run.update(
86
+ state: result[:state],
87
+ current_node: result[:current_node],
88
+ status: result[:terminal] ? :succeeded : :running
89
+ )
90
+
91
+ result
92
+ end
93
+
94
+ # Create a store for LangGraphRB state persistence
95
+ def create_store(adapter = nil, options = {})
96
+ adapter ||= configuration.store_adapter
97
+ options ||= configuration.store_options
98
+
99
+ case adapter.to_sym
100
+ when :redis
101
+ require_relative "langgraphrb_rails/stores/redis"
102
+ LanggraphrbRails::Stores::RedisStore.new(options)
103
+ when :active_record
104
+ require_relative "langgraphrb_rails/stores/active_record"
105
+ LanggraphrbRails::Stores::ActiveRecordStore.new(options)
106
+ else
107
+ LangGraphRB::Stores::InMemoryStore.new
108
+ end
109
+ end
110
+ end
111
+ end