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.
- checksums.yaml +7 -0
- data/README.md +816 -0
- data/Rakefile +23 -0
- data/app/assets/javascripts/langgraphrb_rails.js +153 -0
- data/app/assets/stylesheets/langgraphrb_rails.css +95 -0
- data/lib/generators/langgraph_rb/compatibility.rb +71 -0
- data/lib/generators/langgraph_rb/controller/templates/controller.rb +54 -0
- data/lib/generators/langgraph_rb/controller/templates/view.html.erb +101 -0
- data/lib/generators/langgraph_rb/controller_generator.rb +39 -0
- data/lib/generators/langgraph_rb/graph/templates/graph.rb +68 -0
- data/lib/generators/langgraph_rb/graph_generator.rb +23 -0
- data/lib/generators/langgraph_rb/install/templates/README +45 -0
- data/lib/generators/langgraph_rb/install/templates/example_graph.rb +89 -0
- data/lib/generators/langgraph_rb/install/templates/initializer.rb +35 -0
- data/lib/generators/langgraph_rb/install/templates/langgraph_rb.yml +45 -0
- data/lib/generators/langgraph_rb/install_generator.rb +34 -0
- data/lib/generators/langgraph_rb/job/templates/job.rb +38 -0
- data/lib/generators/langgraph_rb/job_generator.rb +27 -0
- data/lib/generators/langgraph_rb/model/templates/migration.rb +12 -0
- data/lib/generators/langgraph_rb/model/templates/model.rb +15 -0
- data/lib/generators/langgraph_rb/model_generator.rb +34 -0
- data/lib/generators/langgraph_rb/task/templates/task.rake +58 -0
- data/lib/generators/langgraph_rb/task_generator.rb +23 -0
- data/lib/generators/langgraphrb_rails/compatibility.rb +71 -0
- data/lib/generators/langgraphrb_rails/controller/templates/controller.rb +30 -0
- data/lib/generators/langgraphrb_rails/controller/templates/view.html.erb +112 -0
- data/lib/generators/langgraphrb_rails/controller_generator.rb +29 -0
- data/lib/generators/langgraphrb_rails/graph/templates/graph.rb +14 -0
- data/lib/generators/langgraphrb_rails/graph/templates/node.rb +16 -0
- data/lib/generators/langgraphrb_rails/graph_generator.rb +48 -0
- data/lib/generators/langgraphrb_rails/install/templates/config.yml +30 -0
- data/lib/generators/langgraphrb_rails/install/templates/example_graph.rb +44 -0
- data/lib/generators/langgraphrb_rails/install/templates/initializer.rb +27 -0
- data/lib/generators/langgraphrb_rails/install_generator.rb +35 -0
- data/lib/generators/langgraphrb_rails/jobs/templates/run_job.rb +45 -0
- data/lib/generators/langgraphrb_rails/jobs_generator.rb +34 -0
- data/lib/generators/langgraphrb_rails/model/templates/migration.rb +20 -0
- data/lib/generators/langgraphrb_rails/model/templates/model.rb +14 -0
- data/lib/generators/langgraphrb_rails/model_generator.rb +30 -0
- data/lib/generators/langgraphrb_rails/persistence/templates/create_langgraph_runs.rb +18 -0
- data/lib/generators/langgraphrb_rails/persistence/templates/langgraph_run.rb +56 -0
- data/lib/generators/langgraphrb_rails/persistence_generator.rb +28 -0
- data/lib/generators/langgraphrb_rails/task/templates/task.rake +30 -0
- data/lib/generators/langgraphrb_rails/task_generator.rb +17 -0
- data/lib/generators/langgraphrb_rails/tracing/templates/traced.rb +45 -0
- data/lib/generators/langgraphrb_rails/tracing_generator.rb +63 -0
- data/lib/langgraphrb_rails/configuration.rb +47 -0
- data/lib/langgraphrb_rails/engine.rb +20 -0
- data/lib/langgraphrb_rails/helper.rb +141 -0
- data/lib/langgraphrb_rails/middleware/streaming.rb +77 -0
- data/lib/langgraphrb_rails/railtie.rb +55 -0
- data/lib/langgraphrb_rails/stores/active_record.rb +51 -0
- data/lib/langgraphrb_rails/stores/redis.rb +57 -0
- data/lib/langgraphrb_rails/test_helper.rb +126 -0
- data/lib/langgraphrb_rails/version.rb +28 -0
- data/lib/langgraphrb_rails.rb +111 -0
- data/lib/tasks/langgraphrb_rails_tasks.rake +62 -0
- 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, '<').replace(/>/g, '>') + '</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
|