The project is in a healthy, maintained state
Provides Rails generators and configuration for integrating langgraph_rb into Rails applications
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Development

Runtime

~> 0.1, >= 0.1.2
>= 6.0.0, <= 8.0.2
~> 3.5, >= 3.5.0
~> 4.0
 Project Readme

LanggraphRB Rails

Rails integration for the langgraph_rb gem. This gem provides generators, helpers, and configuration for seamlessly integrating LangGraphRB into Rails applications.

Gem Version

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:

  1. Create a configuration file at config/langgraphrb_rails.yml
  2. Create an initializer at config/initializers/langgraphrb_rails.rb
  3. Create a directory for your graphs at app/langgraphs
  4. Add an example graph at app/langgraphs/example_graph.rb
  5. 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:

  1. Create a LanggraphRun model for storing graph state
  2. Create a migration for the model with fields for thread_id, graph, current_node, status, state, context, error, and expires_at
  3. 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:

  1. A Langgraph::RunJob for processing graph runs asynchronously
  2. 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:

  1. Create a LangsmithTraced concern that you can include in your nodes
  2. Update the initializer to add the LangSmith observer if the environment variables are present
  3. 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

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. 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.