# LanggraphRB Rails Rails integration for the [langgraph_rb](https://github.com/fulit103/langgraph_rb) gem. This gem provides generators, helpers, and configuration for seamlessly integrating LangGraphRB into Rails applications. [![Gem Version](https://badge.fury.io/rb/langgraphrb_rails.svg)](https://badge.fury.io/rb/langgraphrb_rails) ## 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: ```ruby gem 'langgraphrb_rails' ``` And then execute: ```bash $ bundle install ``` Or install it yourself as: ```bash $ gem install langgraphrb_rails ``` ## Usage ### Setup Run the installation generator: ```bash $ 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: ```bash $ 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: ```ruby 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: ```yaml 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: ```ruby # 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: ```bash $ 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: ```bash $ 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: ```ruby # 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: ```ruby # 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: ```bash $ 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): ```ruby # 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: ```ruby 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: ```bash $ rails generate langgraphrb_rails:task proposal ``` This will create rake tasks for processing and cleanup in `lib/tasks/langgraph_proposal.rake`: ```ruby 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: ```bash $ 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: ```bash $ 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: ```ruby 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: ```erb <%%= langgraph_chat_interface %> ``` For streaming interfaces: ```erb <%%= 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: ```ruby # 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: ```ruby # 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: ```ruby # 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 ```ruby 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 ```ruby 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: ```ruby 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: ```ruby 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: ```ruby 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: ```ruby 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: ```ruby 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 ```ruby # 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 ```ruby # 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 ```ruby 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: ```bash # 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: ```bash # 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: ```bash cd examples/sample_app bundle exec rspec ``` ### Test Coverage To generate test coverage reports: ```bash 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: ```bash $ bundle exec rake minitest ``` To run just the RSpec tests: ```bash $ 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](https://opensource.org/licenses/MIT).