robot_lab-rails 0.2.1 → 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 -173
- data/CHANGELOG.md +10 -0
- data/CLAUDE.md +71 -0
- data/README.md +25 -0
- data/Rakefile +47 -31
- data/docs/guides/rails-integration.md +100 -2
- data/lib/robot_lab/rails/version.rb +1 -1
- metadata +5 -2
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
CHANGED
|
@@ -1,173 +1 @@
|
|
|
1
|
-
|
|
2
|
-
NewCops: enable
|
|
3
|
-
SuggestExtensions: false
|
|
4
|
-
TargetRubyVersion: 4.0
|
|
5
|
-
Exclude:
|
|
6
|
-
- 'examples/**/*'
|
|
7
|
-
- 'vendor/**/*'
|
|
8
|
-
- 'dead_code/**/*'
|
|
9
|
-
|
|
10
|
-
# ── Style: disabled cops ───────────────────────────────────────────────────
|
|
11
|
-
Style/StringLiterals:
|
|
12
|
-
Enabled: false
|
|
13
|
-
|
|
14
|
-
Style/StringLiteralsInInterpolation:
|
|
15
|
-
Enabled: false
|
|
16
|
-
|
|
17
|
-
Style/Documentation:
|
|
18
|
-
Enabled: false
|
|
19
|
-
|
|
20
|
-
# Ruby 4.0 freezes string literals by default
|
|
21
|
-
Style/FrozenStringLiteralComment:
|
|
22
|
-
Enabled: false
|
|
23
|
-
|
|
24
|
-
Style/IfUnlessModifier:
|
|
25
|
-
Enabled: false
|
|
26
|
-
|
|
27
|
-
Style/RescueModifier:
|
|
28
|
-
Enabled: false
|
|
29
|
-
|
|
30
|
-
Style/TrivialAccessors:
|
|
31
|
-
Enabled: false
|
|
32
|
-
|
|
33
|
-
Style/MultilineTernaryOperator:
|
|
34
|
-
Enabled: false
|
|
35
|
-
|
|
36
|
-
Style/SafeNavigation:
|
|
37
|
-
Enabled: false
|
|
38
|
-
|
|
39
|
-
Style/EmptyClassDefinition:
|
|
40
|
-
Enabled: false
|
|
41
|
-
|
|
42
|
-
Style/ClassAndModuleChildren:
|
|
43
|
-
Enabled: false
|
|
44
|
-
|
|
45
|
-
Style/RescueStandardError:
|
|
46
|
-
Enabled: false
|
|
47
|
-
|
|
48
|
-
Style/OneClassPerFile:
|
|
49
|
-
Enabled: false
|
|
50
|
-
|
|
51
|
-
# Both % and format/sprintf are acceptable
|
|
52
|
-
Style/FormatString:
|
|
53
|
-
Enabled: false
|
|
54
|
-
|
|
55
|
-
# String concatenation and interpolation are both acceptable
|
|
56
|
-
Style/StringConcatenation:
|
|
57
|
-
Enabled: false
|
|
58
|
-
|
|
59
|
-
# ── Layout ─────────────────────────────────────────────────────────────────
|
|
60
|
-
Layout/LineLength:
|
|
61
|
-
Max: 140
|
|
62
|
-
|
|
63
|
-
Layout/ExtraSpacing:
|
|
64
|
-
Enabled: false
|
|
65
|
-
|
|
66
|
-
Layout/HashAlignment:
|
|
67
|
-
Enabled: false
|
|
68
|
-
|
|
69
|
-
Layout/FirstHashElementIndentation:
|
|
70
|
-
Enabled: false
|
|
71
|
-
|
|
72
|
-
Layout/EmptyLineAfterGuardClause:
|
|
73
|
-
Enabled: false
|
|
74
|
-
|
|
75
|
-
# ── Naming ─────────────────────────────────────────────────────────────────
|
|
76
|
-
# Single-char params (c, e, n) are acceptable throughout
|
|
77
|
-
Naming/MethodParameterName:
|
|
78
|
-
Enabled: false
|
|
79
|
-
|
|
80
|
-
Naming/VariableNumber:
|
|
81
|
-
Exclude:
|
|
82
|
-
- 'test/**/*'
|
|
83
|
-
|
|
84
|
-
Naming/RescuedExceptionsVariableName:
|
|
85
|
-
Enabled: false
|
|
86
|
-
|
|
87
|
-
# set_results and similar explicit setters are clear and conventional
|
|
88
|
-
Naming/AccessorMethodName:
|
|
89
|
-
Enabled: false
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
# has_tool_calls? and similar are clear and conventional
|
|
93
|
-
Naming/PredicatePrefix:
|
|
94
|
-
Enabled: false
|
|
95
|
-
|
|
96
|
-
# Test helper methods don't need to follow predicate naming rules
|
|
97
|
-
Naming/PredicateMethod:
|
|
98
|
-
Exclude:
|
|
99
|
-
- 'test/**/*'
|
|
100
|
-
|
|
101
|
-
# ── Lint: relax noisy cops on intentional patterns ─────────────────────────
|
|
102
|
-
# Library and framework methods commonly accept args for API/documentation purposes
|
|
103
|
-
Lint/UnusedMethodArgument:
|
|
104
|
-
Enabled: false
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
Lint/EmptyBlock:
|
|
108
|
-
Exclude:
|
|
109
|
-
- 'test/**/*'
|
|
110
|
-
|
|
111
|
-
Lint/ConstantDefinitionInBlock:
|
|
112
|
-
Exclude:
|
|
113
|
-
- 'Rakefile'
|
|
114
|
-
- 'test/**/*'
|
|
115
|
-
|
|
116
|
-
# ── Gemspec ────────────────────────────────────────────────────────────────
|
|
117
|
-
Gemspec/DevelopmentDependencies:
|
|
118
|
-
EnforcedStyle: Gemfile
|
|
119
|
-
|
|
120
|
-
Gemspec/RequiredRubyVersion:
|
|
121
|
-
Enabled: false
|
|
122
|
-
|
|
123
|
-
Gemspec/OrderedDependencies:
|
|
124
|
-
Enabled: false
|
|
125
|
-
|
|
126
|
-
# ── Metrics ────────────────────────────────────────────────────────────────
|
|
127
|
-
# Framework-level code (routers, parsers, orchestrators) is inherently complex.
|
|
128
|
-
# Flog is the primary complexity gate — these RuboCop thresholds catch only
|
|
129
|
-
# egregious outliers without false-positiving every dispatch method.
|
|
130
|
-
|
|
131
|
-
Metrics/MethodLength:
|
|
132
|
-
Max: 35
|
|
133
|
-
CountAsOne:
|
|
134
|
-
- heredoc
|
|
135
|
-
- array
|
|
136
|
-
- hash
|
|
137
|
-
Exclude:
|
|
138
|
-
- 'test/**/*'
|
|
139
|
-
|
|
140
|
-
Metrics/AbcSize:
|
|
141
|
-
Max: 40
|
|
142
|
-
Exclude:
|
|
143
|
-
- 'test/**/*'
|
|
144
|
-
|
|
145
|
-
Metrics/ClassLength:
|
|
146
|
-
Max: 600
|
|
147
|
-
Exclude:
|
|
148
|
-
- 'test/**/*'
|
|
149
|
-
|
|
150
|
-
Metrics/ModuleLength:
|
|
151
|
-
Max: 200
|
|
152
|
-
Exclude:
|
|
153
|
-
- 'test/**/*'
|
|
154
|
-
|
|
155
|
-
Metrics/CyclomaticComplexity:
|
|
156
|
-
Max: 20
|
|
157
|
-
Exclude:
|
|
158
|
-
- 'test/**/*'
|
|
159
|
-
|
|
160
|
-
Metrics/PerceivedComplexity:
|
|
161
|
-
Max: 20
|
|
162
|
-
Exclude:
|
|
163
|
-
- 'test/**/*'
|
|
164
|
-
|
|
165
|
-
# Long method signatures with keyword args are a Ruby framework idiom
|
|
166
|
-
Metrics/ParameterLists:
|
|
167
|
-
Enabled: false
|
|
168
|
-
|
|
169
|
-
Metrics/BlockLength:
|
|
170
|
-
Exclude:
|
|
171
|
-
- 'Rakefile'
|
|
172
|
-
- '*.gemspec'
|
|
173
|
-
- 'test/**/*'
|
|
1
|
+
inherit_from: ../.rubocop-base.yml
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## [0.2.1] - 2026-05-19
|
|
4
14
|
|
|
5
15
|
### Added
|
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
|
@@ -24,16 +24,6 @@ task :test_file, [:file] do |_t, args|
|
|
|
24
24
|
ruby "test/#{args[:file]}"
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
desc 'Check code style with RuboCop'
|
|
28
|
-
task :rubocop do
|
|
29
|
-
sh 'bundle exec rubocop'
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
desc 'Auto-correct RuboCop offenses'
|
|
33
|
-
task :rubocop_fix do
|
|
34
|
-
sh 'bundle exec rubocop -a'
|
|
35
|
-
end
|
|
36
|
-
|
|
37
27
|
desc 'Check code complexity with Flog (warn >=20, fail >=50)'
|
|
38
28
|
task :flog_check do
|
|
39
29
|
require 'flog'
|
|
@@ -71,36 +61,62 @@ task :flog_check do
|
|
|
71
61
|
end
|
|
72
62
|
end
|
|
73
63
|
|
|
74
|
-
desc '
|
|
75
|
-
task :
|
|
76
|
-
|
|
64
|
+
desc 'Check for structural code duplication with Flay (mass >= 50)'
|
|
65
|
+
task :flay_check do
|
|
66
|
+
require 'flay'
|
|
77
67
|
|
|
78
|
-
|
|
79
|
-
puts 'Quality Gate: Tests + Coverage'
|
|
80
|
-
puts '=' * 60
|
|
81
|
-
results[:tests] = system('bundle exec rake test') ? :pass : :fail
|
|
68
|
+
mass_threshold = 50
|
|
82
69
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
results[:rubocop] = system('bundle exec rubocop') ? :pass : :fail
|
|
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
|
|
87
73
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
92
102
|
|
|
93
103
|
puts "\n#{'=' * 60}"
|
|
94
|
-
puts 'Quality Summary'
|
|
104
|
+
puts 'Quality Gate Summary'
|
|
95
105
|
puts '=' * 60
|
|
96
|
-
results.each do |
|
|
97
|
-
|
|
98
|
-
puts " [#{
|
|
106
|
+
results.each do |label, status|
|
|
107
|
+
badge = status == :pass ? green.call('PASS') : red.call('FAIL')
|
|
108
|
+
puts " [#{badge}] #{label.ljust(width)}"
|
|
99
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)}"
|
|
100
116
|
puts '=' * 60
|
|
101
117
|
|
|
102
|
-
abort "\
|
|
103
|
-
puts "\
|
|
118
|
+
abort "\n#{red.call('Quality gate failed.')}" unless failed.zero?
|
|
119
|
+
puts "\n#{green.call('All quality gates passed.')}"
|
|
104
120
|
end
|
|
105
121
|
|
|
106
122
|
namespace :docs do
|
|
@@ -121,8 +121,15 @@ RobotLab.config.streaming_enabled #=> true
|
|
|
121
121
|
rails generate robot_lab:robot Support
|
|
122
122
|
rails generate robot_lab:robot Billing --description="Handles billing inquiries"
|
|
123
123
|
rails generate robot_lab:robot Router --routing
|
|
124
|
+
rails generate robot_lab:robot Support --tools=order_lookup,refund_processor
|
|
124
125
|
```
|
|
125
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
|
+
|
|
126
133
|
### Robot Class
|
|
127
134
|
|
|
128
135
|
Robots are plain Ruby classes with a `.build` factory method that calls `RobotLab.build` with keyword arguments:
|
|
@@ -503,6 +510,83 @@ class ProcessMessageJob < ApplicationJob
|
|
|
503
510
|
end
|
|
504
511
|
```
|
|
505
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
|
+
|
|
506
590
|
## Testing
|
|
507
591
|
|
|
508
592
|
### Test Configuration
|
|
@@ -582,6 +666,10 @@ class RobotLabThread < ApplicationRecord
|
|
|
582
666
|
def last_result
|
|
583
667
|
results.order(sequence_number: :desc).first
|
|
584
668
|
end
|
|
669
|
+
|
|
670
|
+
def message_count
|
|
671
|
+
results.count
|
|
672
|
+
end
|
|
585
673
|
end
|
|
586
674
|
```
|
|
587
675
|
|
|
@@ -596,14 +684,24 @@ class RobotLabResult < ApplicationRecord
|
|
|
596
684
|
|
|
597
685
|
validates :session_id, presence: true
|
|
598
686
|
validates :robot_name, presence: true
|
|
687
|
+
validates :sequence_number, presence: true,
|
|
688
|
+
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
|
599
689
|
|
|
600
690
|
default_scope { order(sequence_number: :asc) }
|
|
601
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
|
+
|
|
602
700
|
def to_robot_result
|
|
603
701
|
RobotLab::RobotResult.new(
|
|
604
702
|
robot_name: robot_name,
|
|
605
|
-
output:
|
|
606
|
-
tool_calls:
|
|
703
|
+
output: output_messages,
|
|
704
|
+
tool_calls: tool_call_messages,
|
|
607
705
|
stop_reason: stop_reason
|
|
608
706
|
)
|
|
609
707
|
end
|
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.2.
|
|
4
|
+
version: 0.2.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -75,8 +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"
|
|
78
80
|
- ".rubocop.yml"
|
|
79
81
|
- CHANGELOG.md
|
|
82
|
+
- CLAUDE.md
|
|
80
83
|
- LICENSE.txt
|
|
81
84
|
- README.md
|
|
82
85
|
- Rakefile
|
|
@@ -153,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
153
156
|
- !ruby/object:Gem::Version
|
|
154
157
|
version: '0'
|
|
155
158
|
requirements: []
|
|
156
|
-
rubygems_version: 4.0.
|
|
159
|
+
rubygems_version: 4.0.17
|
|
157
160
|
specification_version: 4
|
|
158
161
|
summary: Rails integration for the robot_lab LLM agent framework
|
|
159
162
|
test_files: []
|