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
data/README.md ADDED
@@ -0,0 +1,816 @@
1
+ # LanggraphRB Rails
2
+
3
+ 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.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/langgraphrb_rails.svg)](https://badge.fury.io/rb/langgraphrb_rails)
6
+
7
+ ## Features
8
+
9
+ - **Rails Integration**: Seamlessly integrate LangGraphRB with Rails applications
10
+ - **Generators**: Create graphs, nodes, controllers, models, and more with simple commands
11
+ - **Persistence**: Store graph state in Redis or ActiveRecord
12
+ - **Background Jobs**: Process graph runs asynchronously with ActiveJob
13
+ - **Tracing**: Optional integration with LangSmith for tracing and monitoring
14
+ - **Configuration**: Simple YAML-based configuration
15
+
16
+ ## Supported Rails Versions
17
+
18
+ The gem supports Rails versions 6.0.0 through 8.0.x.
19
+
20
+ ### Compatibility Matrix
21
+
22
+ | LanggraphRB Rails Version | Rails Version | Ruby Version | Status |
23
+ |---------------------------|---------------|--------------|-------------|
24
+ | 0.1.0 | 6.0.x | 2.7+ | Compatible |
25
+ | 0.1.0 | 6.1.x | 2.7+ | Compatible |
26
+ | 0.1.0 | 7.0.x | 3.0+ | Compatible |
27
+ | 0.1.0 | 7.1.x | 3.0+ | Compatible |
28
+ | 0.1.0 | 8.0.x | 3.1+ | Compatible |
29
+
30
+ ### Compatibility Notes
31
+
32
+ - **Rails 7.1.x**: Previous issues with `ActionView::Template::Handlers::ERB::ENCODING_FLAG` have been resolved in version 0.1.0.
33
+
34
+ - **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.
35
+
36
+ ## Installation
37
+
38
+ Add this line to your application's Gemfile:
39
+
40
+ ```ruby
41
+ gem 'langgraphrb_rails'
42
+ ```
43
+
44
+ And then execute:
45
+
46
+ ```bash
47
+ $ bundle install
48
+ ```
49
+
50
+ Or install it yourself as:
51
+
52
+ ```bash
53
+ $ gem install langgraphrb_rails
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ ### Setup
59
+
60
+ Run the installation generator:
61
+
62
+ ```bash
63
+ $ rails generate langgraphrb_rails:install
64
+ ```
65
+
66
+ This will:
67
+
68
+ 1. Create a configuration file at `config/langgraphrb_rails.yml`
69
+ 2. Create an initializer at `config/initializers/langgraphrb_rails.rb`
70
+ 3. Create a directory for your graphs at `app/langgraphs`
71
+ 4. Add an example graph at `app/langgraphs/example_graph.rb`
72
+ 5. Add `app/langgraphs` to your autoload paths
73
+
74
+ ### Creating Graphs
75
+
76
+ To create a new graph:
77
+
78
+ ```bash
79
+ $ rails generate langgraphrb_rails:graph chat
80
+ ```
81
+
82
+ This will create a new graph at `app/graphs/chat_graph.rb`.
83
+
84
+ ### Using Graphs
85
+
86
+ You can use your graphs in your controllers:
87
+
88
+ ```ruby
89
+ class ChatController < ApplicationController
90
+ def create
91
+ result = ChatGraph.invoke(input: params[:message])
92
+ render json: { response: result[:response] }
93
+ end
94
+
95
+ def stream
96
+ response.headers['Content-Type'] = 'text/event-stream'
97
+ response.headers['Last-Modified'] = Time.now.httpdate
98
+
99
+ ChatGraph.stream(input: params[:message]) do |step_result|
100
+ response.stream.write("data: #{step_result.to_json}\n\n")
101
+
102
+ if step_result[:completed]
103
+ response.stream.close
104
+ end
105
+ end
106
+ end
107
+ end
108
+ ```
109
+
110
+ ## Configuration
111
+
112
+ You can configure LanggraphRB Rails in the `config/langgraphrb_rails.yml` file:
113
+
114
+ ```yaml
115
+ default: &default
116
+ store:
117
+ adapter: active_record
118
+ options:
119
+ model: LanggraphRun
120
+ job:
121
+ queue: langgraph
122
+ max_retries: 3
123
+ error:
124
+ policy: retry
125
+ max_retries: 3
126
+
127
+ development:
128
+ <<: *default
129
+
130
+ test:
131
+ <<: *default
132
+ store:
133
+ adapter: memory
134
+
135
+ production:
136
+ <<: *default
137
+ store:
138
+ adapter: redis
139
+ options:
140
+ url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
141
+ namespace: langgraph
142
+ ttl: 86400 # 24 hours
143
+ ```
144
+
145
+ You can also configure the gem programmatically in the initializer:
146
+
147
+ ```ruby
148
+ # config/initializers/langgraphrb_rails.rb
149
+
150
+ LanggraphrbRails.configure do |config|
151
+ # Configure the store adapter
152
+ config.store_adapter = :active_record
153
+ config.store_options = { model: 'LanggraphRun' }
154
+
155
+ # Configure job settings
156
+ config.job_queue = :langgraph
157
+ config.error_policy = :retry
158
+ config.max_retries = 3
159
+
160
+ # Add observers (optional)
161
+ if defined?(LangsmithrbRails) && ENV['LANGSMITH_API_KEY'].present?
162
+ config.observers << LangsmithrbRails::Observer.new(
163
+ api_key: ENV['LANGSMITH_API_KEY'],
164
+ project_name: ENV['LANGSMITH_PROJECT'] || 'langgraphrb-rails'
165
+ )
166
+ end
167
+ end
168
+
169
+ # Add app/langgraphs to autoload paths
170
+ Rails.autoloaders.main.push_dir(Rails.root.join('app', 'langgraphs'))
171
+ ```
172
+
173
+ ### Creating Controllers
174
+
175
+ To create a controller for your graph:
176
+
177
+ ```bash
178
+ $ rails generate langgraphrb_rails:controller chats index create show
179
+ ```
180
+
181
+ This will create a controller with the specified actions, views, and routes.
182
+
183
+ ### Setting Up Persistence
184
+
185
+ To set up persistence for your graphs:
186
+
187
+ ```bash
188
+ $ rails generate langgraphrb_rails:persistence
189
+ $ rails db:migrate
190
+ ```
191
+
192
+ This will:
193
+
194
+ 1. Create a `LanggraphRun` model for storing graph state
195
+ 2. Create a migration for the model with fields for thread_id, graph, current_node, status, state, context, error, and expires_at
196
+ 3. Configure the persistence layer for your graphs
197
+
198
+ You can then use the persistence layer to track and resume graph runs:
199
+
200
+ ```ruby
201
+ # Start a new graph run
202
+ run = LanggraphrbRails.start!(
203
+ graph: "ProposalDraftGraph",
204
+ context: { "user_id" => current_user.id },
205
+ state: {}
206
+ )
207
+
208
+ # Later, resume the run
209
+ LanggraphrbRails.resume!(run.thread_id)
210
+
211
+ # Or find and resume by thread_id
212
+ LanggraphrbRails.resume!(thread_id: "some-thread-id")
213
+ ```
214
+
215
+ The `LanggraphRun` model includes useful scopes:
216
+
217
+ ```ruby
218
+ # Find recent runs
219
+ LanggraphRun.recent
220
+
221
+ # Find expired runs
222
+ LanggraphRun.expired
223
+
224
+ # Find runs by status
225
+ LanggraphRun.queued
226
+ LanggraphRun.running
227
+ LanggraphRun.completed
228
+ LanggraphRun.failed
229
+ ```
230
+
231
+ ### Setting Up Background Jobs
232
+
233
+ To set up background jobs for your graphs:
234
+
235
+ ```bash
236
+ $ rails generate langgraphrb_rails:jobs
237
+ ```
238
+
239
+ This will create:
240
+
241
+ 1. A `Langgraph::RunJob` for processing graph runs asynchronously
242
+ 2. Configuration for the job queue in the initializer
243
+
244
+ 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):
245
+
246
+ ```ruby
247
+ # Start a new graph run
248
+ run = LanggraphrbRails.start!(
249
+ graph: "ProposalDraftGraph",
250
+ context: { "user_id" => current_user.id },
251
+ state: {}
252
+ )
253
+
254
+ # Process the run in the background
255
+ Langgraph::RunJob.perform_later(run.thread_id)
256
+ ```
257
+
258
+ The job includes retry logic and will handle errors according to your configured error policy:
259
+
260
+ ```ruby
261
+ module Langgraph
262
+ class RunJob < ApplicationJob
263
+ queue_as { LanggraphrbRails.configuration.job_queue || :default }
264
+
265
+ retry_on StandardError, wait: :exponentially_longer, attempts: -> { LanggraphrbRails.configuration.max_retries || 3 }
266
+
267
+ def perform(thread_id)
268
+ run = LanggraphRun.find_by(thread_id: thread_id)
269
+ return unless run
270
+
271
+ # Process the run
272
+ LanggraphrbRails.resume!(thread_id: thread_id)
273
+
274
+ # Re-queue the job if the run is not in a terminal state
275
+ unless run.reload.terminal?
276
+ self.class.set(wait: 1.second).perform_later(thread_id)
277
+ end
278
+ end
279
+ end
280
+ end
281
+ ```
282
+
283
+ ### Rake Tasks
284
+
285
+ To create rake tasks for batch processing and cleanup of graph runs:
286
+
287
+ ```bash
288
+ $ rails generate langgraphrb_rails:task proposal
289
+ ```
290
+
291
+ This will create rake tasks for processing and cleanup in `lib/tasks/langgraph_proposal.rake`:
292
+
293
+ ```ruby
294
+ namespace :langgraph_proposal do
295
+ desc "Process pending Proposal graph runs"
296
+ task process: :environment do
297
+ puts "Processing pending Proposal graph runs..."
298
+ count = 0
299
+
300
+ LanggraphRun.where(graph: "ProposalGraph", status: :queued).find_each do |run|
301
+ Langgraph::RunJob.perform_later(run.thread_id)
302
+ count += 1
303
+ end
304
+
305
+ puts "Queued #{count} Proposal graph runs for processing."
306
+ end
307
+
308
+ desc "Clean up expired Proposal graph runs"
309
+ task cleanup: :environment do
310
+ puts "Cleaning up expired Proposal graph runs..."
311
+ count = LanggraphRun.where(graph: "ProposalGraph").expired.delete_all
312
+ puts "Deleted #{count} expired Proposal graph runs."
313
+ end
314
+ end
315
+ ```
316
+
317
+ You can run these tasks with:
318
+
319
+ ```bash
320
+ $ rails langgraph_proposal:process # Process pending tasks
321
+ $ rails langgraph_proposal:cleanup # Clean up expired states
322
+ ```
323
+
324
+ These tasks are useful for scheduled jobs or cron tasks to periodically process queued runs and clean up expired data.
325
+
326
+ ### Setting Up Tracing
327
+
328
+ To set up LangSmith tracing for your graphs:
329
+
330
+ ```bash
331
+ $ rails generate langgraphrb_rails:tracing
332
+ ```
333
+
334
+ This will:
335
+
336
+ 1. Create a `LangsmithTraced` concern that you can include in your nodes
337
+ 2. Update the initializer to add the LangSmith observer if the environment variables are present
338
+ 3. Add sample environment variables to your `.env.sample` file
339
+
340
+ To use tracing in your nodes:
341
+
342
+ ```ruby
343
+ class Nodes::ParseRfp < LangGraphRB::Node
344
+ include LangsmithTraced
345
+
346
+ def call(context:, state:)
347
+ # Wrap your node logic in a trace block
348
+ trace(name: "ParseRfp", meta: { user_id: context["user_id"] }) do |run|
349
+ # Your node logic here
350
+ # Access the run object for additional tracing
351
+ run.add_metadata(document_size: state[:document].size)
352
+
353
+ # Return your node result
354
+ { state: { parsed_content: "..." } }
355
+ end
356
+ end
357
+ end
358
+ ```
359
+
360
+ Make sure to set the following environment variables:
361
+
362
+ ```
363
+ LANGSMITH_API_KEY=your_api_key
364
+ LANGSMITH_PROJECT=your_project_name
365
+ ```
366
+
367
+ ### View Helpers
368
+
369
+ The gem provides view helpers for rendering chat interfaces:
370
+
371
+ ```erb
372
+ <%%= langgraph_chat_interface %>
373
+ ```
374
+
375
+ For streaming interfaces:
376
+
377
+ ```erb
378
+ <%%= langgraph_streaming_chat_interface(
379
+ container_id: 'chat-container',
380
+ placeholder: 'Type your message...',
381
+ submit_text: 'Send',
382
+ stream_path: stream_chats_path
383
+ ) %>
384
+ ```
385
+
386
+ These helpers include all necessary HTML, CSS, and JavaScript for a functional chat interface.
387
+
388
+ ### Streaming Middleware
389
+
390
+ The gem includes middleware for handling server-sent events (SSE) streaming:
391
+
392
+ ```ruby
393
+ # config/application.rb
394
+ config.middleware.use LanggraphrbRails::Middleware::Streaming
395
+ ```
396
+
397
+ This middleware automatically handles streaming responses from your graphs.
398
+
399
+ ## Advanced Usage
400
+
401
+ ### Custom Stores
402
+
403
+ You can configure a custom store in your initializer:
404
+
405
+ ```ruby
406
+ # config/initializers/langgraph_rb.rb
407
+
408
+ LanggraphrbRails.configure_store do |store_config|
409
+ store_config.adapter = :redis
410
+ store_config.options = {
411
+ url: ENV['REDIS_URL'],
412
+ namespace: 'langgraph_rb'
413
+ }
414
+ end
415
+ ```
416
+
417
+ The gem supports three store adapters out of the box:
418
+ - `:memory` - In-memory store (default for development)
419
+ - `:redis` - Redis-based store (recommended for production)
420
+ - `:active_record` - ActiveRecord-based store
421
+
422
+ ### Custom Observers
423
+
424
+ You can configure custom observers:
425
+
426
+ ```ruby
427
+ # config/initializers/langgraph_rb.rb
428
+
429
+ LanggraphrbRails.configure_observers do |observers|
430
+ observers << LangGraphRB::Observers::Logger.new
431
+ observers << LangGraphRB::Observers::Structured.new
432
+ observers << MyCustomObserver.new
433
+ end
434
+ ```
435
+
436
+ ## Testing
437
+
438
+ The gem includes RSpec support for testing your graphs and nodes:
439
+
440
+ ### Testing Graphs
441
+
442
+ ```ruby
443
+ require 'rails_helper'
444
+
445
+ RSpec.describe ProposalDraftGraph do
446
+ # Use a memory store for testing
447
+ before do
448
+ allow(LanggraphrbRails).to receive(:create_store).and_return(LangGraphRB::Storage::Memory.new)
449
+ end
450
+
451
+ describe "#invoke" do
452
+ it "processes an RFP document" do
453
+ # Create test data
454
+ rfp_text = "Request for Proposal: AI-powered analytics platform"
455
+
456
+ # Invoke the graph
457
+ result = described_class.invoke(rfp_text: rfp_text)
458
+
459
+ # Verify the result
460
+ expect(result[:state]).to include(:proposal)
461
+ expect(result[:state][:proposal]).to be_a(String)
462
+ expect(result[:state][:proposal]).to include("Analytics")
463
+ end
464
+ end
465
+
466
+ describe "persistence" do
467
+ it "can be resumed from a saved state" do
468
+ # Start a graph run
469
+ run = LanggraphrbRails.start!(
470
+ graph: "ProposalDraftGraph",
471
+ context: { rfp_text: "Sample RFP" },
472
+ state: {}
473
+ )
474
+
475
+ # Resume the run
476
+ result = LanggraphrbRails.resume!(run.thread_id)
477
+
478
+ # Verify the result
479
+ expect(result).to be_a(LanggraphRun)
480
+ expect(result.state).to include("proposal")
481
+ end
482
+ end
483
+ end
484
+ ```
485
+
486
+ ### Testing Nodes
487
+
488
+ ```ruby
489
+ require 'rails_helper'
490
+
491
+ RSpec.describe Nodes::ParseRfp do
492
+ describe "#call" do
493
+ it "extracts key information from an RFP" do
494
+ # Create a node instance
495
+ node = described_class.new
496
+
497
+ # Call the node with test data
498
+ result = node.call(
499
+ context: { "user_id" => 1 },
500
+ state: { rfp_text: "Request for Proposal: AI-powered analytics platform" }
501
+ )
502
+
503
+ # Verify the result
504
+ expect(result[:state]).to include(:rfp_details)
505
+ expect(result[:state][:rfp_details]).to include(:title)
506
+ expect(result[:state][:rfp_details][:title]).to include("analytics")
507
+ end
508
+ end
509
+ end
510
+ ```
511
+
512
+ ### Testing with Mocks
513
+
514
+ You can mock external dependencies in your tests:
515
+
516
+ ```ruby
517
+ RSpec.describe Nodes::GenerateProposal do
518
+ describe "#call" do
519
+ it "generates a proposal based on RFP details" do
520
+ # Mock any external services
521
+ allow_any_instance_of(OpenAI::Client).to receive(:chat).and_return(
522
+ { "choices" => [{ "message" => { "content" => "Mock proposal content" } }] }
523
+ )
524
+
525
+ # Create a node instance
526
+ node = described_class.new
527
+
528
+ # Call the node with test data
529
+ result = node.call(
530
+ context: {},
531
+ state: { rfp_details: { title: "AI Analytics", requirements: ["Real-time data"] } }
532
+ )
533
+
534
+ # Verify the result
535
+ expect(result[:state]).to include(:proposal)
536
+ expect(result[:state][:proposal]).to eq("Mock proposal content")
537
+ end
538
+ end
539
+ end
540
+ ```
541
+
542
+ ## Unit Testing Approach
543
+
544
+ 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.
545
+
546
+ ### Rails Mock Helper
547
+
548
+ The `RailsMockHelper` module provides mocks for Rails components:
549
+
550
+ ```ruby
551
+ require 'spec_helper'
552
+
553
+ RSpec.describe YourGenerator, type: :generator do
554
+ include RailsMockHelper
555
+
556
+ before(:each) do
557
+ setup_rails_mocks
558
+ # Your test setup
559
+ end
560
+
561
+ # Your tests
562
+ end
563
+ ```
564
+
565
+ ### Dummy App Helper
566
+
567
+ The `DummyAppHelper` module creates a minimal directory structure for testing generators:
568
+
569
+ ```ruby
570
+ RSpec.describe YourGenerator, type: :generator do
571
+ include RailsMockHelper
572
+ include DummyAppHelper
573
+
574
+ before(:each) do
575
+ setup_rails_mocks
576
+ setup_dummy_directories
577
+
578
+ # Stub generator methods
579
+ allow_any_instance_of(described_class).to receive(:template) do |_, source, dest|
580
+ rel_path = dest.to_s.sub("#{Rails.root}/", '')
581
+ create_dummy_file(rel_path, "# Generated from #{source}")
582
+ end
583
+ end
584
+
585
+ after(:each) do
586
+ cleanup_dummy_directories
587
+ end
588
+
589
+ # Your tests
590
+ end
591
+ ```
592
+
593
+ ### Testing Generators
594
+
595
+ Generator tests verify that the correct files are created with the expected content:
596
+
597
+ ```ruby
598
+ it "creates the expected files" do
599
+ run_generator(["MyGraph", "--nodes=node1,node2"])
600
+
601
+ expect(File.exist?(File.join(Rails.root, 'app/langgraphs/my_graph.rb'))).to be true
602
+ expect(File.exist?(File.join(Rails.root, 'app/langgraph_nodes/nodes/node1.rb'))).to be true
603
+ expect(File.exist?(File.join(Rails.root, 'app/langgraph_nodes/nodes/node2.rb'))).to be true
604
+ end
605
+ ```
606
+
607
+ ### Testing Persistence Adapters
608
+
609
+ Persistence adapter tests use mocks to verify storage and retrieval functionality:
610
+
611
+ ```ruby
612
+ RSpec.describe LanggraphrbRails::Persistence::ActiveRecordStore do
613
+ before(:each) do
614
+ @langgraph_run = double("LanggraphRun")
615
+ allow(LanggraphRun).to receive(:find_by).and_return(@langgraph_run)
616
+
617
+ @store = described_class.new
618
+ end
619
+
620
+ it "retrieves state from the database" do
621
+ allow(@langgraph_run).to receive(:state).and_return({"key" => "value"})
622
+
623
+ result = @store.get("thread-123")
624
+ expect(result).to eq({"key" => "value"})
625
+ end
626
+ end
627
+ ```
628
+
629
+ ## Example Implementation
630
+
631
+ Here's a simple example of how to implement a graph with LanggraphRB Rails:
632
+
633
+ ### Graph Definition
634
+
635
+ ```ruby
636
+ # app/langgraphs/proposal_draft_graph.rb
637
+ class ProposalDraftGraph < LanggraphrbRails::Graph
638
+ def build
639
+ # Define nodes
640
+ add_node :parse_rfp, Nodes::ParseRfp.new
641
+ add_node :generate_proposal, Nodes::GenerateProposal.new
642
+ add_node :review_and_finalize, Nodes::ReviewAndFinalize.new
643
+
644
+ # Define edges
645
+ add_edge :parse_rfp, :generate_proposal
646
+ add_edge :generate_proposal, :review_and_finalize
647
+
648
+ # Set terminal node
649
+ set_entry_point :parse_rfp
650
+ set_terminal_nodes :review_and_finalize
651
+ end
652
+ end
653
+ ```
654
+
655
+ ### Node Implementation
656
+
657
+ ```ruby
658
+ # app/langgraph_nodes/nodes/parse_rfp.rb
659
+ module Nodes
660
+ class ParseRfp < LanggraphrbRails::Node
661
+ def call(context:, state:)
662
+ # Extract RFP details from text
663
+ rfp_details = {
664
+ title: extract_title(state[:rfp_text]),
665
+ requirements: extract_requirements(state[:rfp_text])
666
+ }
667
+
668
+ # Return updated state
669
+ { state: state.merge(rfp_details: rfp_details) }
670
+ end
671
+
672
+ private
673
+
674
+ def extract_title(text)
675
+ # Implementation
676
+ end
677
+
678
+ def extract_requirements(text)
679
+ # Implementation
680
+ end
681
+ end
682
+ end
683
+ ```
684
+
685
+ ### Controller Integration
686
+
687
+ ```ruby
688
+ class RfpsController < ApplicationController
689
+ def create
690
+ # Start a graph run with the uploaded RFP
691
+ run = LanggraphrbRails.start!(
692
+ graph: "ProposalDraftGraph",
693
+ context: { user_id: current_user.id },
694
+ state: { rfp_text: params[:rfp][:content] }
695
+ )
696
+
697
+ # Process in background
698
+ Langgraph::RunJob.perform_later(run.thread_id)
699
+
700
+ redirect_to rfp_path(run.thread_id)
701
+ end
702
+
703
+ def show
704
+ @run = LanggraphRun.find_by(thread_id: params[:id])
705
+ end
706
+ end
707
+ ```
708
+
709
+ ### Creating Your Own Application
710
+
711
+ Follow these steps to create a new Rails app that uses langgraphrb_rails:
712
+
713
+ ```bash
714
+ # Create a new Rails app
715
+ rails new my_app -d postgresql
716
+ cd my_app
717
+ bin/rails db:create
718
+
719
+ # Add gems to Gemfile
720
+ gem "langgraphrb"
721
+ gem "langgraphrb_rails"
722
+ gem "langsmithrb_rails" # for tracing (optional)
723
+
724
+ bundle install
725
+
726
+ # Run generators
727
+ bin/rails g langgraphrb_rails:install
728
+ bin/rails g langgraphrb_rails:persistence
729
+ bin/rails g langgraphrb_rails:jobs
730
+
731
+ # Create a graph with nodes
732
+ bin/rails g langgraphrb_rails:graph MyGraph --nodes=node1,node2,node3
733
+
734
+ # Create a controller for your graph
735
+ bin/rails g langgraphrb_rails:controller my_graphs index show create
736
+
737
+ # Create a model for your data
738
+ bin/rails g langgraphrb_rails:model document title:string content:text
739
+
740
+ # Create rake tasks for batch processing
741
+ bin/rails g langgraphrb_rails:task my_graph
742
+
743
+ # Optional: Set up LangSmith tracing
744
+ bin/rails g langgraphrb_rails:tracing
745
+
746
+ # Run migrations
747
+ bin/rails db:migrate
748
+ ```
749
+
750
+ ## Contributing
751
+
752
+ 1. Fork it
753
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
754
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
755
+ 4. Push to the branch (`git push origin my-new-feature`)
756
+ 5. Create new Pull Request
757
+
758
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cdaviis/langgraphrb_rails.
759
+
760
+ ## Running Tests
761
+
762
+ The gem includes a comprehensive test suite using RSpec. To run all tests:
763
+
764
+ ```bash
765
+ # Run all tests
766
+ bundle exec rspec
767
+
768
+ # Run specific test files
769
+ bundle exec rspec spec/langgraphrb_rails_spec.rb
770
+
771
+ # Run specific test groups
772
+ bundle exec rspec spec/generators/
773
+
774
+ # Run with documentation format
775
+ bundle exec rspec --format documentation
776
+ ```
777
+
778
+ ### Testing the Example App
779
+
780
+ The example application also includes tests:
781
+
782
+ ```bash
783
+ cd examples/sample_app
784
+ bundle exec rspec
785
+ ```
786
+
787
+ ### Test Coverage
788
+
789
+ To generate test coverage reports:
790
+
791
+ ```bash
792
+ BUNDLE_GEMFILE=Gemfile.coverage bundle install
793
+ BUNDLE_GEMFILE=Gemfile.coverage bundle exec rspec
794
+ ```
795
+
796
+ This will generate a coverage report in the `coverage` directory.
797
+
798
+ To run just the Minitest tests:
799
+
800
+ ```bash
801
+ $ bundle exec rake minitest
802
+ ```
803
+
804
+ To run just the RSpec tests:
805
+
806
+ ```bash
807
+ $ bundle exec rake spec
808
+ ```
809
+
810
+ ## Contributing
811
+
812
+ 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.
813
+
814
+ ## License
815
+
816
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).