robot_lab-rails 0.1.0 → 0.2.6
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 +4 -4
- data/.envrc +2 -0
- data/.loki +15 -0
- data/.quality/reek_baseline.txt +3 -0
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +26 -0
- data/CLAUDE.md +71 -0
- data/README.md +25 -0
- data/Rakefile +127 -3
- data/docs/guides/rails-integration.md +111 -6
- data/lib/robot_lab/rails/version.rb +1 -1
- data/lib/robot_lab/rails.rb +5 -1
- data/lib/robot_lab/rails_integration/job.rb +4 -3
- data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +2 -2
- metadata +10 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 363024a9b53fc93e6bee9f45d93ade96b62f8e2fcc86172eb1233baea141b525
|
|
4
|
+
data.tar.gz: 3ad6bf319772440866cb90ac41169e99eff28e9137143281685c48e46d652774
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec1fcbe29db8f114c0ce38a942a1582d1d018ac0a834412368763cb02d4bd5e3886cfa55da2d914d2408e7dca553bc1c86c63512f4f717bcb7e8a8dafa1b4ba2
|
|
7
|
+
data.tar.gz: 5332b0bd26088e36c01610ceae13b34ca3850c3a58a91a82a4fac264239f3dfae6b9625ace7c0f495e91e7d60d31c9b1ac98ce80aa561131d27021a35725d0fe
|
data/.envrc
CHANGED
data/.loki
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# robot_lab-rails — ActiveJob, generators, Turbo broadcasts for Rails apps.
|
|
3
|
+
|
|
4
|
+
import_up "repo_dev.loki"
|
|
5
|
+
|
|
6
|
+
class Tasks
|
|
7
|
+
@@gem_name ||= "robot_lab-rails".freeze
|
|
8
|
+
|
|
9
|
+
header "robot_lab-rails v#{gem_version} — Rails integration for RobotLab"
|
|
10
|
+
|
|
11
|
+
desc "Run Rails-specific integration tests"
|
|
12
|
+
def integration
|
|
13
|
+
sh "bundle exec rake test"
|
|
14
|
+
end
|
|
15
|
+
end
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
inherit_from: ../.rubocop-base.yml
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
### Added
|
|
4
|
+
- `.loki` Asgard task file: `test`, `rubocop`, `rubocop_fix`, `flog`, `flay`, `quality`, `build`, `install`, `release`, and `console` tasks via the Asgard task runner
|
|
5
|
+
- `flay_check` Rake task: structural code duplication gate (mass threshold 50); integrated into the `quality` Rake task
|
|
6
|
+
- `flay` and `minitest-reporters` gems added to development dependencies
|
|
7
|
+
- `test_output.txt`, `flay_output.txt`, `flog_output.txt`, and `rubocop_output.txt` added to `.gitignore`
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- `test/test_helper.rb`: test output redirected to `test_output.txt` via `$stdout` reassignment; `TerminalSummaryReporter` prints a single PASS/FAIL summary line to the terminal
|
|
11
|
+
- `Rakefile`: `rubocop` and `rubocop_fix` tasks removed (now owned by Asgard); `flay_check` integrated into the `quality` gate
|
|
12
|
+
|
|
13
|
+
## [0.2.1] - 2026-05-19
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Rails Engine — autoloads `app/robots/` and `app/tools/` in the host application via Zeitwerk
|
|
17
|
+
- Railtie — wires `RobotLab.config.logger` to `Rails.logger` on boot and applies Rails-specific defaults
|
|
18
|
+
- `RobotLab::Job` (`RobotLab::RailsIntegration::Job`) — ActiveJob base class handling robot-class resolution, `RobotLabThread` persistence, Turbo Stream wiring, and completion/error broadcasting
|
|
19
|
+
- `robot_class` DSL on `RobotLab::Job` subclasses — binds a dedicated job to a specific robot class without requiring `robot_class:` at enqueue time
|
|
20
|
+
- `TurboStreamCallbacks` — streams content tokens and tool-call badges to `#robot_response` and `#robot_tools` targets in real time; graceful no-op when `turbo-rails` is absent
|
|
21
|
+
- Generator `robot_lab:install` — creates initializer, migration, models (`RobotLabThread`, `RobotLabResult`), `RobotRunJob`, and `app/robots/` / `app/tools/` directories
|
|
22
|
+
- Generator `robot_lab:robot NAME` — generates a robot class stub in `app/robots/`
|
|
23
|
+
- Generator `robot_lab:job NAME [--queue QUEUE]` — generates a dedicated job subclass pre-bound to a robot class
|
|
24
|
+
- Default retry/discard policy on `RobotLab::Job`: `retry_on StandardError` (3 attempts, 5 s wait); `discard_on ActiveJob::DeserializationError`
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Version synchronized with robot_lab core 0.2.1
|
|
28
|
+
|
|
3
29
|
## [0.1.0] - 2026-05-07
|
|
4
30
|
|
|
5
31
|
- Initial release
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What This Gem Does
|
|
6
|
+
|
|
7
|
+
`robot_lab-rails` is a Rails Engine that integrates RobotLab into Rails applications. It provides a Railtie for configuration, an ActiveJob base class with optional Turbo Stream broadcasting, generators for scaffolding, and autoloading for `app/robots/` and `app/tools/`.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle exec rake test # Run full test suite
|
|
13
|
+
ruby -Ilib:test test/<file> # Run a single test file
|
|
14
|
+
|
|
15
|
+
# Inside a Rails app using this gem:
|
|
16
|
+
rails g robot_lab:install # migration, models, initializer, job
|
|
17
|
+
rails g robot_lab:robot MyRobot # app/robots/my_robot.rb + test
|
|
18
|
+
rails g robot_lab:robot MyRouter --routing # routing robot variant
|
|
19
|
+
rails g robot_lab:job MyRobot # app/jobs/my_robot_job.rb
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
**`RailsIntegration::Engine`** (`rails_integration/engine.rb`) — Rails Engine with `isolate_namespace RobotLab`. Adds `app/robots/` and `app/tools/` to Rails autoload paths so robot and tool classes are autoloaded like models.
|
|
25
|
+
|
|
26
|
+
**`RailsIntegration::Railtie`** (`rails_integration/railtie.rb`) — Reads `config.robot_lab.default_model` and `config.robot_lab.default_provider` from the Rails app config and applies them to `RobotLab.configure`. Sets `RobotLab.config.logger = Rails.logger` so all RobotLab log output goes through Rails logging.
|
|
27
|
+
|
|
28
|
+
**`RailsIntegration::Job`** (`rails_integration/job.rb`) — `ActiveJob::Base` subclass. Subclass with `robot_class MyRobot` DSL, or pass `robot_class:` at enqueue time.
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
class SupportRobotJob < RobotLab::Job
|
|
32
|
+
queue_as :default
|
|
33
|
+
robot_class SupportRobot
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
SupportRobotJob.perform_later(message: "Hello", thread_id: session_id)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
When `thread_id` is provided and Turbo is available, the job streams content and tool-call badges live via `Turbo::StreamsChannel`, and persists the result to `RobotLabThread` / `RobotLabResult` ActiveRecord models. Built-in retry (3 attempts, 5 second wait) and discard on `DeserializationError`.
|
|
40
|
+
|
|
41
|
+
**`RailsIntegration::TurboStreamCallbacks`** (`rails_integration/turbo_stream_callbacks.rb`) — Stateless module. Safe to require without turbo-rails installed (guards with `defined?(Turbo::StreamsChannel)` at call time).
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
cb = TurboStreamCallbacks.build_content_callback(stream_name: "robot_lab_thread_42")
|
|
45
|
+
# => Proc that broadcasts each content chunk as HTML-escaped text
|
|
46
|
+
|
|
47
|
+
tool_cb = TurboStreamCallbacks.build_tool_call_callback(stream_name: "robot_lab_thread_42")
|
|
48
|
+
# => Proc that broadcasts a tool-badge span per tool call
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Generator Templates
|
|
52
|
+
|
|
53
|
+
Templates live in `lib/generators/robot_lab/templates/`. The install generator creates:
|
|
54
|
+
- `config/initializers/robot_lab.rb`
|
|
55
|
+
- `db/migrate/create_robot_lab_tables.rb` (RobotLabThread + RobotLabResult tables)
|
|
56
|
+
- `app/models/robot_lab_thread.rb` / `app/models/robot_lab_result.rb`
|
|
57
|
+
- `app/jobs/robot_run_job.rb`
|
|
58
|
+
- Empty `app/robots/` and `app/tools/` directories
|
|
59
|
+
|
|
60
|
+
## Key Constraints
|
|
61
|
+
|
|
62
|
+
- `Engine` and `Railtie` are only loaded when `defined?(Rails)` — the gem is safe to require in non-Rails contexts (e.g. test helpers that stub Rails).
|
|
63
|
+
- `Job#setup_thread` calls `"RobotLabThread".constantize` — this is a string reference to avoid a hard dependency; ensure the model is generated before using `thread_id`.
|
|
64
|
+
- `TurboStreamCallbacks` HTML-escapes all content via `ERB::Util.html_escape` — do not bypass this.
|
|
65
|
+
- The Railtie uses `ActiveSupport.on_load(:active_record)` for AR setup — place any ActiveRecord extensions there, not in the initializer.
|
|
66
|
+
|
|
67
|
+
## Testing
|
|
68
|
+
|
|
69
|
+
- Minitest with SimpleCov (branch coverage tracked, no minimum threshold enforced yet)
|
|
70
|
+
- Tests stub Rails, ActiveJob, and Turbo — no Rails app is loaded
|
|
71
|
+
- Coverage baseline: ~80% line / ~82% branch
|
data/README.md
CHANGED
|
@@ -78,6 +78,31 @@ SupportJob.perform_later(message: params[:message], thread_id: session_id)
|
|
|
78
78
|
|
|
79
79
|
Omitting `thread_id` runs the robot in fire-and-forget mode — no persistence, no broadcasting.
|
|
80
80
|
|
|
81
|
+
## Recurring Tasks (Solid Queue)
|
|
82
|
+
|
|
83
|
+
When using [Solid Queue](https://github.com/rails/solid_queue), skip the wrapper job and call a class method directly via the `command:` key in `config/recurring.yml`:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# app/services/daily_digest.rb
|
|
87
|
+
class DailyDigest
|
|
88
|
+
def self.run
|
|
89
|
+
User.digest_subscribers.find_each do |user|
|
|
90
|
+
DailyDigestJob.perform_later(user_id: user.id)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
# config/recurring.yml
|
|
98
|
+
daily_digest:
|
|
99
|
+
command: "DailyDigest.run"
|
|
100
|
+
schedule: "every day at 06:00"
|
|
101
|
+
queue: ai # optional; defaults to solid_queue_recurring
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The orchestrator class is independently testable. For overnight autonomous runs, `RobotLab::To.run` works as a recurring command directly. See the [Rails Integration Guide](https://madbomber.github.io/robot_lab-rails/guides/rails-integration#recurring-tasks-solid-queue) for queue configuration details and the `robot_lab-to` pattern.
|
|
105
|
+
|
|
81
106
|
## Turbo Stream Streaming
|
|
82
107
|
|
|
83
108
|
When `thread_id` is provided and [turbo-rails](https://github.com/hotwired/turbo-rails) is installed, `RobotLab::Job` automatically:
|
data/Rakefile
CHANGED
|
@@ -1,8 +1,132 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rake/testtask'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << 'test'
|
|
8
|
+
t.libs << 'lib'
|
|
9
|
+
t.test_files = FileList['test/**/*_test.rb', 'test/**/test_*.rb'].exclude('**/*_helper.rb')
|
|
10
|
+
t.verbose = true
|
|
11
|
+
t.ruby_opts << '-rtest_helper'
|
|
12
|
+
end
|
|
7
13
|
|
|
8
14
|
task default: :test
|
|
15
|
+
|
|
16
|
+
desc 'Run tests with verbose output'
|
|
17
|
+
task :test_verbose do
|
|
18
|
+
ENV['TESTOPTS'] = '--verbose'
|
|
19
|
+
Rake::Task[:test].invoke
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc 'Run a single test file'
|
|
23
|
+
task :test_file, [:file] do |_t, args|
|
|
24
|
+
ruby "test/#{args[:file]}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc 'Check code complexity with Flog (warn >=20, fail >=50)'
|
|
28
|
+
task :flog_check do
|
|
29
|
+
require 'flog'
|
|
30
|
+
|
|
31
|
+
method_warn = 20.0
|
|
32
|
+
method_fail = 50.0
|
|
33
|
+
|
|
34
|
+
flogger = Flog.new(all: true)
|
|
35
|
+
flogger.flog(*Dir.glob('lib/**/*.rb'))
|
|
36
|
+
|
|
37
|
+
warnings = []
|
|
38
|
+
failures = []
|
|
39
|
+
|
|
40
|
+
flogger.each_by_score do |method, score|
|
|
41
|
+
next if method.end_with?('#none')
|
|
42
|
+
|
|
43
|
+
if score > method_fail
|
|
44
|
+
failures << "#{format('%.1f', score)}: #{method}"
|
|
45
|
+
elsif score > method_warn
|
|
46
|
+
warnings << "#{format('%.1f', score)}: #{method}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
unless warnings.empty?
|
|
51
|
+
puts "\nFlog warnings (#{method_warn}–#{method_fail}) — target for future refactoring:"
|
|
52
|
+
warnings.each { |v| puts " #{v}" }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if failures.empty?
|
|
56
|
+
puts "\nFlog: no methods exceed the failure threshold (>=#{method_fail})"
|
|
57
|
+
else
|
|
58
|
+
puts "\nFlog failures (>=#{method_fail}) — must be refactored:"
|
|
59
|
+
failures.each { |v| puts " #{v}" }
|
|
60
|
+
abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{method_fail}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
desc 'Check for structural code duplication with Flay (mass >= 50)'
|
|
65
|
+
task :flay_check do
|
|
66
|
+
require 'flay'
|
|
67
|
+
|
|
68
|
+
mass_threshold = 50
|
|
69
|
+
|
|
70
|
+
flay = Flay.new({ mass: mass_threshold, diff: false, verbose: false, summary: false, timeout: 60 })
|
|
71
|
+
flay.process(*Dir.glob('lib/**/*.rb'))
|
|
72
|
+
flay.analyze
|
|
73
|
+
|
|
74
|
+
if flay.hashes.empty?
|
|
75
|
+
puts "\nFlay: no structural duplication detected (mass >= #{mass_threshold})"
|
|
76
|
+
else
|
|
77
|
+
puts "\nFlay found structural duplication (mass >= #{mass_threshold}):"
|
|
78
|
+
flay.report
|
|
79
|
+
abort "\nFlay quality gate failed: #{flay.hashes.length} pattern(s) detected"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
desc 'Run all quality checks: tests (with coverage), RuboCop, Flog, and Flay'
|
|
84
|
+
task :quality do
|
|
85
|
+
gates = [
|
|
86
|
+
['Tests + Coverage', 'bundle exec rake test'],
|
|
87
|
+
['RuboCop', 'bundle exec rubocop'],
|
|
88
|
+
['Flog Complexity', 'bundle exec rake flog_check'],
|
|
89
|
+
['Flay Duplication', 'bundle exec rake flay_check']
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
results = gates.map do |label, command|
|
|
93
|
+
puts "\n#{'=' * 60}"
|
|
94
|
+
puts "Quality Gate: #{label}"
|
|
95
|
+
puts '=' * 60
|
|
96
|
+
[label, system(command) ? :pass : :fail]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
green = ->(s) { "\e[32m#{s}\e[0m" }
|
|
100
|
+
red = ->(s) { "\e[31m#{s}\e[0m" }
|
|
101
|
+
width = results.map { |label, _| label.length }.max
|
|
102
|
+
|
|
103
|
+
puts "\n#{'=' * 60}"
|
|
104
|
+
puts 'Quality Gate Summary'
|
|
105
|
+
puts '=' * 60
|
|
106
|
+
results.each do |label, status|
|
|
107
|
+
badge = status == :pass ? green.call('PASS') : red.call('FAIL')
|
|
108
|
+
puts " [#{badge}] #{label.ljust(width)}"
|
|
109
|
+
end
|
|
110
|
+
puts '-' * 60
|
|
111
|
+
|
|
112
|
+
passed = results.count { |_, s| s == :pass }
|
|
113
|
+
failed = results.count { |_, s| s == :fail }
|
|
114
|
+
tally = "#{passed} passed, #{failed} failed"
|
|
115
|
+
puts " #{failed.zero? ? green.call(tally) : red.call(tally)}"
|
|
116
|
+
puts '=' * 60
|
|
117
|
+
|
|
118
|
+
abort "\n#{red.call('Quality gate failed.')}" unless failed.zero?
|
|
119
|
+
puts "\n#{green.call('All quality gates passed.')}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
namespace :docs do
|
|
123
|
+
desc 'Build MkDocs documentation'
|
|
124
|
+
task :build do
|
|
125
|
+
sh 'mkdocs build'
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
desc 'Serve MkDocs documentation locally on http://localhost:8000'
|
|
129
|
+
task :serve do
|
|
130
|
+
sh 'mkdocs serve'
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -35,7 +35,7 @@ rails db:migrate
|
|
|
35
35
|
|
|
36
36
|
## Configuration
|
|
37
37
|
|
|
38
|
-
RobotLab uses [MywayConfig](https://github.com/madbomber/myway_config) for configuration.
|
|
38
|
+
RobotLab uses [MywayConfig](https://github.com/madbomber/myway_config) for static configuration. Settings are loaded from YAML files and environment variables in the following priority order:
|
|
39
39
|
|
|
40
40
|
1. **Bundled defaults** (`lib/robot_lab/config/defaults.yml`)
|
|
41
41
|
2. **Environment-specific overrides** (development, test, production sections)
|
|
@@ -85,14 +85,21 @@ ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180
|
|
|
85
85
|
|
|
86
86
|
RobotLab also falls back to standard provider environment variables (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`) when the prefixed versions are not set.
|
|
87
87
|
|
|
88
|
-
### Initializer
|
|
88
|
+
### Initializer
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
Use `RobotLab.configure` to set runtime attributes. The generated initializer wires the logger to `Rails.logger`:
|
|
91
91
|
|
|
92
92
|
```ruby title="config/initializers/robot_lab.rb"
|
|
93
93
|
# frozen_string_literal: true
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
RobotLab.configure do |c|
|
|
96
|
+
c.logger = Rails.logger
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
You can also assign it directly if you prefer a one-liner:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
96
103
|
RobotLab.config.logger = Rails.logger
|
|
97
104
|
```
|
|
98
105
|
|
|
@@ -114,8 +121,15 @@ RobotLab.config.streaming_enabled #=> true
|
|
|
114
121
|
rails generate robot_lab:robot Support
|
|
115
122
|
rails generate robot_lab:robot Billing --description="Handles billing inquiries"
|
|
116
123
|
rails generate robot_lab:robot Router --routing
|
|
124
|
+
rails generate robot_lab:robot Support --tools=order_lookup,refund_processor
|
|
117
125
|
```
|
|
118
126
|
|
|
127
|
+
Options:
|
|
128
|
+
|
|
129
|
+
- `--description` — Robot description (defaults to `"A helpful <Name> robot"`)
|
|
130
|
+
- `--routing` — Generate a routing robot (subclasses `RobotLab::Robot`, overrides `call`) instead of the plain factory-style robot
|
|
131
|
+
- `--tools` — List of tool names; emitted as commented-out entries in the generated `tools` array for you to uncomment and wire up
|
|
132
|
+
|
|
119
133
|
### Robot Class
|
|
120
134
|
|
|
121
135
|
Robots are plain Ruby classes with a `.build` factory method that calls `RobotLab.build` with keyword arguments:
|
|
@@ -496,6 +510,83 @@ class ProcessMessageJob < ApplicationJob
|
|
|
496
510
|
end
|
|
497
511
|
```
|
|
498
512
|
|
|
513
|
+
## Recurring Tasks (Solid Queue)
|
|
514
|
+
|
|
515
|
+
When using [Solid Queue](https://github.com/rails/solid_queue), you can schedule robot runs without writing a thin wrapper job. Solid Queue's `command:` key in `config/recurring.yml` calls a class method directly, eliminating unnecessary glue code.
|
|
516
|
+
|
|
517
|
+
### Orchestrator class
|
|
518
|
+
|
|
519
|
+
Keep the scheduling logic in a plain Ruby class. The class method fans out per-item jobs — or runs a robot directly:
|
|
520
|
+
|
|
521
|
+
```ruby title="app/services/daily_digest.rb"
|
|
522
|
+
class DailyDigest
|
|
523
|
+
def self.run
|
|
524
|
+
User.digest_subscribers.find_each do |user|
|
|
525
|
+
DailyDigestJob.perform_later(user_id: user.id)
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Wiring in recurring.yml
|
|
532
|
+
|
|
533
|
+
Reference the method directly in `config/recurring.yml`:
|
|
534
|
+
|
|
535
|
+
```yaml title="config/recurring.yml"
|
|
536
|
+
daily_digest:
|
|
537
|
+
command: "DailyDigest.run"
|
|
538
|
+
schedule: "every day at 06:00"
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
No wrapper job needed. `DailyDigest.run` is a plain class method — independently testable without enqueuing anything.
|
|
542
|
+
|
|
543
|
+
### Queue configuration
|
|
544
|
+
|
|
545
|
+
Solid Queue sends recurring commands to the `solid_queue_recurring` queue by default. Add a dedicated worker in `config/queue.yml` so recurring runs don't compete with your main job queues:
|
|
546
|
+
|
|
547
|
+
```yaml title="config/queue.yml"
|
|
548
|
+
production:
|
|
549
|
+
workers:
|
|
550
|
+
- queues: [solid_queue_recurring]
|
|
551
|
+
threads: 1
|
|
552
|
+
- queues: [default, ai]
|
|
553
|
+
threads: 3
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Override the queue per task with `queue:` to route recurring robot runs through a shared worker:
|
|
557
|
+
|
|
558
|
+
```yaml title="config/recurring.yml"
|
|
559
|
+
daily_digest:
|
|
560
|
+
command: "DailyDigest.run"
|
|
561
|
+
schedule: "every day at 06:00"
|
|
562
|
+
queue: ai
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Using robot_lab-to for overnight runs
|
|
566
|
+
|
|
567
|
+
The `robot_lab-to` gem's `RobotLab::To.run` is designed for a single autonomous session, making it a natural fit for a Solid Queue recurring task:
|
|
568
|
+
|
|
569
|
+
```ruby title="app/services/nightly_analysis.rb"
|
|
570
|
+
class NightlyAnalysis
|
|
571
|
+
def self.run
|
|
572
|
+
RobotLab::To.run(
|
|
573
|
+
"Analyze yesterday's support tickets and generate a quality report",
|
|
574
|
+
max_iterations: 10,
|
|
575
|
+
stop_when: "Report is written to disk"
|
|
576
|
+
)
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
```yaml title="config/recurring.yml"
|
|
582
|
+
nightly_analysis:
|
|
583
|
+
command: "NightlyAnalysis.run"
|
|
584
|
+
schedule: "every day at 02:00"
|
|
585
|
+
queue: ai
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
> `RobotLab::To.run` is synchronous. Solid Queue runs it inside a worker process — this is intentional. Each nightly run blocks its worker for the duration of the autonomous loop.
|
|
589
|
+
|
|
499
590
|
## Testing
|
|
500
591
|
|
|
501
592
|
### Test Configuration
|
|
@@ -575,6 +666,10 @@ class RobotLabThread < ApplicationRecord
|
|
|
575
666
|
def last_result
|
|
576
667
|
results.order(sequence_number: :desc).first
|
|
577
668
|
end
|
|
669
|
+
|
|
670
|
+
def message_count
|
|
671
|
+
results.count
|
|
672
|
+
end
|
|
578
673
|
end
|
|
579
674
|
```
|
|
580
675
|
|
|
@@ -589,14 +684,24 @@ class RobotLabResult < ApplicationRecord
|
|
|
589
684
|
|
|
590
685
|
validates :session_id, presence: true
|
|
591
686
|
validates :robot_name, presence: true
|
|
687
|
+
validates :sequence_number, presence: true,
|
|
688
|
+
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
|
592
689
|
|
|
593
690
|
default_scope { order(sequence_number: :asc) }
|
|
594
691
|
|
|
692
|
+
def output_messages
|
|
693
|
+
(output_data || []).map { |d| RobotLab::Message.from_hash(d.symbolize_keys) }
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def tool_call_messages
|
|
697
|
+
(tool_calls_data || []).map { |d| RobotLab::Message.from_hash(d.symbolize_keys) }
|
|
698
|
+
end
|
|
699
|
+
|
|
595
700
|
def to_robot_result
|
|
596
701
|
RobotLab::RobotResult.new(
|
|
597
702
|
robot_name: robot_name,
|
|
598
|
-
output:
|
|
599
|
-
tool_calls:
|
|
703
|
+
output: output_messages,
|
|
704
|
+
tool_calls: tool_call_messages,
|
|
600
705
|
stop_reason: stop_reason
|
|
601
706
|
)
|
|
602
707
|
end
|
data/lib/robot_lab/rails.rb
CHANGED
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
require_relative "rails/version"
|
|
4
4
|
require_relative "rails_integration/turbo_stream_callbacks"
|
|
5
5
|
|
|
6
|
-
if defined?(
|
|
6
|
+
if defined?(Rails)
|
|
7
7
|
require_relative "rails_integration/engine"
|
|
8
8
|
require_relative "rails_integration/railtie"
|
|
9
9
|
require_relative "rails_integration/job"
|
|
10
10
|
end
|
|
11
|
+
|
|
12
|
+
if defined?(RobotLab) && RobotLab.respond_to?(:register_extension)
|
|
13
|
+
RobotLab.register_extension(:rails, RobotLab::Rails)
|
|
14
|
+
end
|
|
@@ -39,9 +39,10 @@ module RobotLab
|
|
|
39
39
|
|
|
40
40
|
def resolve_robot_class(runtime_class)
|
|
41
41
|
klass = runtime_class || self.class.robot_class
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
unless klass
|
|
43
|
+
raise ArgumentError,
|
|
44
|
+
"No robot class specified. Pass robot_class: to perform or set robot_class on the job class."
|
|
45
|
+
end
|
|
45
46
|
|
|
46
47
|
return klass if klass.is_a?(Class)
|
|
47
48
|
|
|
@@ -13,7 +13,7 @@ module RobotLab
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def self.build_content_callback(stream_name:, target: "robot_response")
|
|
16
|
-
|
|
16
|
+
lambda { |chunk|
|
|
17
17
|
content = chunk.respond_to?(:content) ? chunk.content : chunk.to_s
|
|
18
18
|
return unless content && TurboStreamCallbacks.available?
|
|
19
19
|
|
|
@@ -26,7 +26,7 @@ module RobotLab
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def self.build_tool_call_callback(stream_name:, target: "robot_tools")
|
|
29
|
-
|
|
29
|
+
lambda { |tool_call|
|
|
30
30
|
return unless TurboStreamCallbacks.available?
|
|
31
31
|
|
|
32
32
|
name = tool_call.respond_to?(:name) ? tool_call.name : tool_call.to_s
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: robot_lab-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -13,16 +13,16 @@ dependencies:
|
|
|
13
13
|
name: robot_lab
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: 0.2.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
25
|
+
version: 0.2.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: railties
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -75,7 +75,11 @@ extra_rdoc_files: []
|
|
|
75
75
|
files:
|
|
76
76
|
- ".envrc"
|
|
77
77
|
- ".github/workflows/deploy-github-pages.yml"
|
|
78
|
+
- ".loki"
|
|
79
|
+
- ".quality/reek_baseline.txt"
|
|
80
|
+
- ".rubocop.yml"
|
|
78
81
|
- CHANGELOG.md
|
|
82
|
+
- CLAUDE.md
|
|
79
83
|
- LICENSE.txt
|
|
80
84
|
- README.md
|
|
81
85
|
- Rakefile
|
|
@@ -152,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
152
156
|
- !ruby/object:Gem::Version
|
|
153
157
|
version: '0'
|
|
154
158
|
requirements: []
|
|
155
|
-
rubygems_version: 4.0.
|
|
159
|
+
rubygems_version: 4.0.17
|
|
156
160
|
specification_version: 4
|
|
157
161
|
summary: Rails integration for the robot_lab LLM agent framework
|
|
158
162
|
test_files: []
|