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
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
|
+
[](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).
|