LanggraphRB Rails
Rails integration for the langgraph_rb gem. This gem provides generators, helpers, and configuration for seamlessly integrating LangGraphRB into Rails applications.
Features
- Rails Integration: Seamlessly integrate LangGraphRB with Rails applications
- Generators: Create graphs, nodes, controllers, models, and more with simple commands
- Persistence: Store graph state in Redis or ActiveRecord
- Background Jobs: Process graph runs asynchronously with ActiveJob
- Tracing: Optional integration with LangSmith for tracing and monitoring
- Configuration: Simple YAML-based configuration
Supported Rails Versions
The gem supports Rails versions 6.0.0 through 8.0.x.
Compatibility Matrix
LanggraphRB Rails Version | Rails Version | Ruby Version | Status |
---|---|---|---|
0.1.0 | 6.0.x | 2.7+ | Compatible |
0.1.0 | 6.1.x | 2.7+ | Compatible |
0.1.0 | 7.0.x | 3.0+ | Compatible |
0.1.0 | 7.1.x | 3.0+ | Compatible |
0.1.0 | 8.0.x | 3.1+ | Compatible |
Compatibility Notes
-
Rails 7.1.x: Previous issues with
ActionView::Template::Handlers::ERB::ENCODING_FLAG
have been resolved in version 0.1.0. -
Rails 8.x: Now supported with version 0.1.0. The gem has been updated to work with the latest Rails 8 features and API changes.
Installation
Add this line to your application's Gemfile:
gem 'langgraphrb_rails'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install langgraphrb_rails
Usage
Setup
Run the installation generator:
$ rails generate langgraphrb_rails:install
This will:
- Create a configuration file at
config/langgraphrb_rails.yml
- Create an initializer at
config/initializers/langgraphrb_rails.rb
- Create a directory for your graphs at
app/langgraphs
- Add an example graph at
app/langgraphs/example_graph.rb
- Add
app/langgraphs
to your autoload paths
Creating Graphs
To create a new graph:
$ rails generate langgraphrb_rails:graph chat
This will create a new graph at app/graphs/chat_graph.rb
.
Using Graphs
You can use your graphs in your controllers:
class ChatController < ApplicationController
def create
result = ChatGraph.invoke(input: params[:message])
render json: { response: result[:response] }
end
def stream
response.headers['Content-Type'] = 'text/event-stream'
response.headers['Last-Modified'] = Time.now.httpdate
ChatGraph.stream(input: params[:message]) do |step_result|
response.stream.write("data: #{step_result.to_json}\n\n")
if step_result[:completed]
response.stream.close
end
end
end
end
Configuration
You can configure LanggraphRB Rails in the config/langgraphrb_rails.yml
file:
default: &default
store:
adapter: active_record
options:
model: LanggraphRun
job:
queue: langgraph
max_retries: 3
error:
policy: retry
max_retries: 3
development:
<<: *default
test:
<<: *default
store:
adapter: memory
production:
<<: *default
store:
adapter: redis
options:
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
namespace: langgraph
ttl: 86400 # 24 hours
You can also configure the gem programmatically in the initializer:
# config/initializers/langgraphrb_rails.rb
LanggraphrbRails.configure do |config|
# Configure the store adapter
config.store_adapter = :active_record
config.store_options = { model: 'LanggraphRun' }
# Configure job settings
config.job_queue = :langgraph
config.error_policy = :retry
config.max_retries = 3
# Add observers (optional)
if defined?(LangsmithrbRails) && ENV['LANGSMITH_API_KEY'].present?
config.observers << LangsmithrbRails::Observer.new(
api_key: ENV['LANGSMITH_API_KEY'],
project_name: ENV['LANGSMITH_PROJECT'] || 'langgraphrb-rails'
)
end
end
# Add app/langgraphs to autoload paths
Rails.autoloaders.main.push_dir(Rails.root.join('app', 'langgraphs'))
Creating Controllers
To create a controller for your graph:
$ rails generate langgraphrb_rails:controller chats index create show
This will create a controller with the specified actions, views, and routes.
Setting Up Persistence
To set up persistence for your graphs:
$ rails generate langgraphrb_rails:persistence
$ rails db:migrate
This will:
- Create a
LanggraphRun
model for storing graph state - Create a migration for the model with fields for thread_id, graph, current_node, status, state, context, error, and expires_at
- Configure the persistence layer for your graphs
You can then use the persistence layer to track and resume graph runs:
# Start a new graph run
run = LanggraphrbRails.start!(
graph: "ProposalDraftGraph",
context: { "user_id" => current_user.id },
state: {}
)
# Later, resume the run
LanggraphrbRails.resume!(run.thread_id)
# Or find and resume by thread_id
LanggraphrbRails.resume!(thread_id: "some-thread-id")
The LanggraphRun
model includes useful scopes:
# Find recent runs
LanggraphRun.recent
# Find expired runs
LanggraphRun.expired
# Find runs by status
LanggraphRun.queued
LanggraphRun.running
LanggraphRun.completed
LanggraphRun.failed
Setting Up Background Jobs
To set up background jobs for your graphs:
$ rails generate langgraphrb_rails:jobs
This will create:
- A
Langgraph::RunJob
for processing graph runs asynchronously - Configuration for the job queue in the initializer
The job is designed to handle graph runs asynchronously and will automatically re-queue itself if the run is not in a terminal state (completed or failed):
# Start a new graph run
run = LanggraphrbRails.start!(
graph: "ProposalDraftGraph",
context: { "user_id" => current_user.id },
state: {}
)
# Process the run in the background
Langgraph::RunJob.perform_later(run.thread_id)
The job includes retry logic and will handle errors according to your configured error policy:
module Langgraph
class RunJob < ApplicationJob
queue_as { LanggraphrbRails.configuration.job_queue || :default }
retry_on StandardError, wait: :exponentially_longer, attempts: -> { LanggraphrbRails.configuration.max_retries || 3 }
def perform(thread_id)
run = LanggraphRun.find_by(thread_id: thread_id)
return unless run
# Process the run
LanggraphrbRails.resume!(thread_id: thread_id)
# Re-queue the job if the run is not in a terminal state
unless run.reload.terminal?
self.class.set(wait: 1.second).perform_later(thread_id)
end
end
end
end
Rake Tasks
To create rake tasks for batch processing and cleanup of graph runs:
$ rails generate langgraphrb_rails:task proposal
This will create rake tasks for processing and cleanup in lib/tasks/langgraph_proposal.rake
:
namespace :langgraph_proposal do
desc "Process pending Proposal graph runs"
task process: :environment do
puts "Processing pending Proposal graph runs..."
count = 0
LanggraphRun.where(graph: "ProposalGraph", status: :queued).find_each do |run|
Langgraph::RunJob.perform_later(run.thread_id)
count += 1
end
puts "Queued #{count} Proposal graph runs for processing."
end
desc "Clean up expired Proposal graph runs"
task cleanup: :environment do
puts "Cleaning up expired Proposal graph runs..."
count = LanggraphRun.where(graph: "ProposalGraph").expired.delete_all
puts "Deleted #{count} expired Proposal graph runs."
end
end
You can run these tasks with:
$ rails langgraph_proposal:process # Process pending tasks
$ rails langgraph_proposal:cleanup # Clean up expired states
These tasks are useful for scheduled jobs or cron tasks to periodically process queued runs and clean up expired data.
Setting Up Tracing
To set up LangSmith tracing for your graphs:
$ rails generate langgraphrb_rails:tracing
This will:
- Create a
LangsmithTraced
concern that you can include in your nodes - Update the initializer to add the LangSmith observer if the environment variables are present
- Add sample environment variables to your
.env.sample
file
To use tracing in your nodes:
class Nodes::ParseRfp < LangGraphRB::Node
include LangsmithTraced
def call(context:, state:)
# Wrap your node logic in a trace block
trace(name: "ParseRfp", meta: { user_id: context["user_id"] }) do |run|
# Your node logic here
# Access the run object for additional tracing
run.add_metadata(document_size: state[:document].size)
# Return your node result
{ state: { parsed_content: "..." } }
end
end
end
Make sure to set the following environment variables:
LANGSMITH_API_KEY=your_api_key
LANGSMITH_PROJECT=your_project_name
View Helpers
The gem provides view helpers for rendering chat interfaces:
<%%= langgraph_chat_interface %>
For streaming interfaces:
<%%= langgraph_streaming_chat_interface(
container_id: 'chat-container',
placeholder: 'Type your message...',
submit_text: 'Send',
stream_path: stream_chats_path
) %>
These helpers include all necessary HTML, CSS, and JavaScript for a functional chat interface.
Streaming Middleware
The gem includes middleware for handling server-sent events (SSE) streaming:
# config/application.rb
config.middleware.use LanggraphrbRails::Middleware::Streaming
This middleware automatically handles streaming responses from your graphs.
Advanced Usage
Custom Stores
You can configure a custom store in your initializer:
# config/initializers/langgraph_rb.rb
LanggraphrbRails.configure_store do |store_config|
store_config.adapter = :redis
store_config.options = {
url: ENV['REDIS_URL'],
namespace: 'langgraph_rb'
}
end
The gem supports three store adapters out of the box:
-
:memory
- In-memory store (default for development) -
:redis
- Redis-based store (recommended for production) -
:active_record
- ActiveRecord-based store
Custom Observers
You can configure custom observers:
# config/initializers/langgraph_rb.rb
LanggraphrbRails.configure_observers do |observers|
observers << LangGraphRB::Observers::Logger.new
observers << LangGraphRB::Observers::Structured.new
observers << MyCustomObserver.new
end
Testing
The gem includes RSpec support for testing your graphs and nodes:
Testing Graphs
require 'rails_helper'
RSpec.describe ProposalDraftGraph do
# Use a memory store for testing
before do
allow(LanggraphrbRails).to receive(:create_store).and_return(LangGraphRB::Storage::Memory.new)
end
describe "#invoke" do
it "processes an RFP document" do
# Create test data
rfp_text = "Request for Proposal: AI-powered analytics platform"
# Invoke the graph
result = described_class.invoke(rfp_text: rfp_text)
# Verify the result
expect(result[:state]).to include(:proposal)
expect(result[:state][:proposal]).to be_a(String)
expect(result[:state][:proposal]).to include("Analytics")
end
end
describe "persistence" do
it "can be resumed from a saved state" do
# Start a graph run
run = LanggraphrbRails.start!(
graph: "ProposalDraftGraph",
context: { rfp_text: "Sample RFP" },
state: {}
)
# Resume the run
result = LanggraphrbRails.resume!(run.thread_id)
# Verify the result
expect(result).to be_a(LanggraphRun)
expect(result.state).to include("proposal")
end
end
end
Testing Nodes
require 'rails_helper'
RSpec.describe Nodes::ParseRfp do
describe "#call" do
it "extracts key information from an RFP" do
# Create a node instance
node = described_class.new
# Call the node with test data
result = node.call(
context: { "user_id" => 1 },
state: { rfp_text: "Request for Proposal: AI-powered analytics platform" }
)
# Verify the result
expect(result[:state]).to include(:rfp_details)
expect(result[:state][:rfp_details]).to include(:title)
expect(result[:state][:rfp_details][:title]).to include("analytics")
end
end
end
Testing with Mocks
You can mock external dependencies in your tests:
RSpec.describe Nodes::GenerateProposal do
describe "#call" do
it "generates a proposal based on RFP details" do
# Mock any external services
allow_any_instance_of(OpenAI::Client).to receive(:chat).and_return(
{ "choices" => [{ "message" => { "content" => "Mock proposal content" } }] }
)
# Create a node instance
node = described_class.new
# Call the node with test data
result = node.call(
context: {},
state: { rfp_details: { title: "AI Analytics", requirements: ["Real-time data"] } }
)
# Verify the result
expect(result[:state]).to include(:proposal)
expect(result[:state][:proposal]).to eq("Mock proposal content")
end
end
end
Unit Testing Approach
The gem uses a lightweight unit testing framework that doesn't require a full Rails application. This approach makes tests faster, more maintainable, and reduces the gem's overall size.
Rails Mock Helper
The RailsMockHelper
module provides mocks for Rails components:
require 'spec_helper'
RSpec.describe YourGenerator, type: :generator do
include RailsMockHelper
before(:each) do
setup_rails_mocks
# Your test setup
end
# Your tests
end
Dummy App Helper
The DummyAppHelper
module creates a minimal directory structure for testing generators:
RSpec.describe YourGenerator, type: :generator do
include RailsMockHelper
include DummyAppHelper
before(:each) do
setup_rails_mocks
setup_dummy_directories
# Stub generator methods
allow_any_instance_of(described_class).to receive(:template) do |_, source, dest|
rel_path = dest.to_s.sub("#{Rails.root}/", '')
create_dummy_file(rel_path, "# Generated from #{source}")
end
end
after(:each) do
cleanup_dummy_directories
end
# Your tests
end
Testing Generators
Generator tests verify that the correct files are created with the expected content:
it "creates the expected files" do
run_generator(["MyGraph", "--nodes=node1,node2"])
expect(File.exist?(File.join(Rails.root, 'app/langgraphs/my_graph.rb'))).to be true
expect(File.exist?(File.join(Rails.root, 'app/langgraph_nodes/nodes/node1.rb'))).to be true
expect(File.exist?(File.join(Rails.root, 'app/langgraph_nodes/nodes/node2.rb'))).to be true
end
Testing Persistence Adapters
Persistence adapter tests use mocks to verify storage and retrieval functionality:
RSpec.describe LanggraphrbRails::Persistence::ActiveRecordStore do
before(:each) do
@langgraph_run = double("LanggraphRun")
allow(LanggraphRun).to receive(:find_by).and_return(@langgraph_run)
@store = described_class.new
end
it "retrieves state from the database" do
allow(@langgraph_run).to receive(:state).and_return({"key" => "value"})
result = @store.get("thread-123")
expect(result).to eq({"key" => "value"})
end
end
Example Implementation
Here's a simple example of how to implement a graph with LanggraphRB Rails:
Graph Definition
# app/langgraphs/proposal_draft_graph.rb
class ProposalDraftGraph < LanggraphrbRails::Graph
def build
# Define nodes
add_node :parse_rfp, Nodes::ParseRfp.new
add_node :generate_proposal, Nodes::GenerateProposal.new
add_node :review_and_finalize, Nodes::ReviewAndFinalize.new
# Define edges
add_edge :parse_rfp, :generate_proposal
add_edge :generate_proposal, :review_and_finalize
# Set terminal node
set_entry_point :parse_rfp
set_terminal_nodes :review_and_finalize
end
end
Node Implementation
# app/langgraph_nodes/nodes/parse_rfp.rb
module Nodes
class ParseRfp < LanggraphrbRails::Node
def call(context:, state:)
# Extract RFP details from text
rfp_details = {
title: extract_title(state[:rfp_text]),
requirements: extract_requirements(state[:rfp_text])
}
# Return updated state
{ state: state.merge(rfp_details: rfp_details) }
end
private
def extract_title(text)
# Implementation
end
def extract_requirements(text)
# Implementation
end
end
end
Controller Integration
class RfpsController < ApplicationController
def create
# Start a graph run with the uploaded RFP
run = LanggraphrbRails.start!(
graph: "ProposalDraftGraph",
context: { user_id: current_user.id },
state: { rfp_text: params[:rfp][:content] }
)
# Process in background
Langgraph::RunJob.perform_later(run.thread_id)
redirect_to rfp_path(run.thread_id)
end
def show
@run = LanggraphRun.find_by(thread_id: params[:id])
end
end
Creating Your Own Application
Follow these steps to create a new Rails app that uses langgraphrb_rails:
# Create a new Rails app
rails new my_app -d postgresql
cd my_app
bin/rails db:create
# Add gems to Gemfile
gem "langgraphrb"
gem "langgraphrb_rails"
gem "langsmithrb_rails" # for tracing (optional)
bundle install
# Run generators
bin/rails g langgraphrb_rails:install
bin/rails g langgraphrb_rails:persistence
bin/rails g langgraphrb_rails:jobs
# Create a graph with nodes
bin/rails g langgraphrb_rails:graph MyGraph --nodes=node1,node2,node3
# Create a controller for your graph
bin/rails g langgraphrb_rails:controller my_graphs index show create
# Create a model for your data
bin/rails g langgraphrb_rails:model document title:string content:text
# Create rake tasks for batch processing
bin/rails g langgraphrb_rails:task my_graph
# Optional: Set up LangSmith tracing
bin/rails g langgraphrb_rails:tracing
# Run migrations
bin/rails db:migrate
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
Bug reports and pull requests are welcome on GitHub at https://github.com/cdaviis/langgraphrb_rails.
Running Tests
The gem includes a comprehensive test suite using RSpec. To run all tests:
# Run all tests
bundle exec rspec
# Run specific test files
bundle exec rspec spec/langgraphrb_rails_spec.rb
# Run specific test groups
bundle exec rspec spec/generators/
# Run with documentation format
bundle exec rspec --format documentation
Testing the Example App
The example application also includes tests:
cd examples/sample_app
bundle exec rspec
Test Coverage
To generate test coverage reports:
BUNDLE_GEMFILE=Gemfile.coverage bundle install
BUNDLE_GEMFILE=Gemfile.coverage bundle exec rspec
This will generate a coverage report in the coverage
directory.
To run just the Minitest tests:
$ bundle exec rake minitest
To run just the RSpec tests:
$ bundle exec rake spec
Contributing
When contributing, please ensure all tests pass and add tests for any new functionality. We prefer RSpec for new tests, but maintain Minitest for backward compatibility.
License
The gem is available as open source under the terms of the MIT License.