robot_lab-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/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +52 -0
  4. data/CHANGELOG.md +5 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +110 -0
  7. data/Rakefile +8 -0
  8. data/docs/examples/rails-application.md +419 -0
  9. data/docs/guides/rails-integration.md +681 -0
  10. data/docs/index.md +75 -0
  11. data/examples/18_rails/.envrc +3 -0
  12. data/examples/18_rails/.gitignore +5 -0
  13. data/examples/18_rails/Gemfile +11 -0
  14. data/examples/18_rails/README.md +48 -0
  15. data/examples/18_rails/Rakefile +4 -0
  16. data/examples/18_rails/app/controllers/application_controller.rb +4 -0
  17. data/examples/18_rails/app/controllers/chat_controller.rb +46 -0
  18. data/examples/18_rails/app/jobs/application_job.rb +4 -0
  19. data/examples/18_rails/app/jobs/robot_run_job.rb +19 -0
  20. data/examples/18_rails/app/models/application_record.rb +5 -0
  21. data/examples/18_rails/app/models/robot_lab_result.rb +36 -0
  22. data/examples/18_rails/app/models/robot_lab_thread.rb +23 -0
  23. data/examples/18_rails/app/robots/chat_robot.rb +14 -0
  24. data/examples/18_rails/app/tools/time_tool.rb +9 -0
  25. data/examples/18_rails/app/views/chat/_user_message.html.erb +1 -0
  26. data/examples/18_rails/app/views/chat/index.html.erb +67 -0
  27. data/examples/18_rails/app/views/layouts/application.html.erb +49 -0
  28. data/examples/18_rails/bin/dev +7 -0
  29. data/examples/18_rails/bin/rails +6 -0
  30. data/examples/18_rails/bin/setup +15 -0
  31. data/examples/18_rails/config/application.rb +33 -0
  32. data/examples/18_rails/config/cable.yml +2 -0
  33. data/examples/18_rails/config/database.yml +5 -0
  34. data/examples/18_rails/config/environment.rb +4 -0
  35. data/examples/18_rails/config/initializers/robot_lab.rb +3 -0
  36. data/examples/18_rails/config/routes.rb +6 -0
  37. data/examples/18_rails/config.ru +4 -0
  38. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +32 -0
  39. data/lib/generators/robot_lab/install_generator.rb +63 -0
  40. data/lib/generators/robot_lab/job_generator.rb +28 -0
  41. data/lib/generators/robot_lab/robot_generator.rb +40 -0
  42. data/lib/generators/robot_lab/templates/initializer.rb.tt +39 -0
  43. data/lib/generators/robot_lab/templates/job.rb.tt +21 -0
  44. data/lib/generators/robot_lab/templates/migration.rb.tt +32 -0
  45. data/lib/generators/robot_lab/templates/result_model.rb.tt +40 -0
  46. data/lib/generators/robot_lab/templates/robot.rb.tt +31 -0
  47. data/lib/generators/robot_lab/templates/robot_job.rb.tt +15 -0
  48. data/lib/generators/robot_lab/templates/robot_test.rb.tt +21 -0
  49. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +42 -0
  50. data/lib/generators/robot_lab/templates/thread_model.rb.tt +27 -0
  51. data/lib/robot_lab/rails/version.rb +7 -0
  52. data/lib/robot_lab/rails.rb +10 -0
  53. data/lib/robot_lab/rails_integration/engine.rb +23 -0
  54. data/lib/robot_lab/rails_integration/job.rb +109 -0
  55. data/lib/robot_lab/rails_integration/railtie.rb +36 -0
  56. data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +42 -0
  57. data/mkdocs.yml +117 -0
  58. metadata +158 -0
data/docs/index.md ADDED
@@ -0,0 +1,75 @@
1
+ # robot_lab-rails
2
+
3
+ Rails integration for the [RobotLab](https://github.com/MadBomber/robot_lab) LLM agent framework.
4
+
5
+ > [!CAUTION]
6
+ > This gem is under active development. APIs may change without notice.
7
+
8
+ ## What it provides
9
+
10
+ - **Rails Engine** — autoloads `app/robots/` and `app/tools/` in your application
11
+ - **Railtie** — wires `RobotLab.config` to `Rails.logger` and applies Rails-specific configuration
12
+ - **`RobotLab::Job`** — ActiveJob base class with Turbo Stream streaming, thread persistence, and error broadcasting
13
+ - **Generators** — `rails generate robot_lab:install`, `robot_lab:robot NAME`, `robot_lab:job NAME`
14
+
15
+ ## Installation
16
+
17
+ Add to your Gemfile:
18
+
19
+ ```ruby
20
+ gem "robot_lab"
21
+ gem "robot_lab-rails"
22
+ ```
23
+
24
+ Then run the installer:
25
+
26
+ ```bash
27
+ rails generate robot_lab:install
28
+ rails db:migrate
29
+ ```
30
+
31
+ ## Quick Example
32
+
33
+ ```ruby
34
+ # app/robots/support_robot.rb
35
+ class SupportRobot
36
+ SYSTEM_PROMPT = "You are a helpful support assistant."
37
+
38
+ def self.build(**options)
39
+ RobotLab.build(
40
+ name: "support",
41
+ system_prompt: SYSTEM_PROMPT,
42
+ **options
43
+ )
44
+ end
45
+ end
46
+
47
+ # app/jobs/support_robot_job.rb
48
+ class SupportRobotJob < RobotLab::Job
49
+ queue_as :default
50
+ robot_class SupportRobot
51
+ end
52
+
53
+ # From a controller:
54
+ SupportRobotJob.perform_later(
55
+ message: params[:message],
56
+ thread_id: session[:id]
57
+ )
58
+ ```
59
+
60
+ ## Turbo Stream Streaming
61
+
62
+ When `thread_id:` is provided and `turbo-rails` is installed, streaming token output is broadcast automatically:
63
+
64
+ ```erb
65
+ <%= turbo_stream_from "robot_lab_thread_#{session[:id]}" %>
66
+ <div id="robot_response"></div>
67
+ <div id="robot_status"></div>
68
+ ```
69
+
70
+ ## Links
71
+
72
+ - [Rails Integration Guide](guides/rails-integration.md)
73
+ - [Rails Application Example](examples/rails-application.md)
74
+ - [RobotLab Core](https://github.com/MadBomber/robot_lab)
75
+ - [RubyGems](https://rubygems.org/gems/robot_lab-rails)
@@ -0,0 +1,3 @@
1
+ source_up
2
+
3
+ export RR=`pwd`
@@ -0,0 +1,5 @@
1
+ db/*.sqlite3
2
+ db/schema.rb
3
+ log/
4
+ tmp/
5
+ Gemfile.lock
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 8.0"
6
+ gem "sqlite3", "~> 2.0"
7
+ gem "puma", "~> 6.0"
8
+ gem "turbo-rails", "~> 2.0"
9
+
10
+ gem "robot_lab"
11
+ gem "robot_lab-rails"
@@ -0,0 +1,48 @@
1
+ # RobotLab Rails 8 Demo
2
+
3
+ Minimal Rails 8 app that demonstrates RobotLab's full Rails integration:
4
+
5
+ - **ChatRobot** with a custom `TimeTool`
6
+ - **RobotRunJob** for background execution
7
+ - **Turbo Stream** token streaming to the browser
8
+ - **Persistence** via `RobotLabThread` + `RobotLabResult`
9
+ - **Conversation history** on page reload
10
+
11
+ ## Prerequisites
12
+
13
+ - Ruby 3.2+
14
+ - An LLM API key (e.g. `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` in your env)
15
+
16
+ ## Setup
17
+
18
+ ```bash
19
+ cd examples/18_rails
20
+ bin/setup
21
+ ```
22
+
23
+ ## Run
24
+
25
+ ```bash
26
+ bin/dev
27
+ # Open http://localhost:3000
28
+ ```
29
+
30
+ ## Try It
31
+
32
+ 1. Type "What time is it?" — the robot will call TimeTool and stream the response
33
+ 2. Refresh the page — conversation history is preserved
34
+ 3. Check `db/development.sqlite3` to see persisted threads and results
35
+
36
+ ## Architecture
37
+
38
+ ```
39
+ app/
40
+ controllers/chat_controller.rb — index + create actions
41
+ views/chat/index.html.erb — form + Turbo Stream subscription
42
+ models/ — RobotLabThread, RobotLabResult
43
+ robots/chat_robot.rb — robot factory with system prompt + tool
44
+ tools/time_tool.rb — simple RobotLab::Tool subclass
45
+ jobs/robot_run_job.rb — enqueues robot.run() with Turbo callbacks
46
+ ```
47
+
48
+ No Redis, no Solid Queue, no asset pipeline. Uses `:async` adapters for both ActiveJob and ActionCable.
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config/application"
4
+ Rails.application.load_tasks
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationController < ActionController::Base
4
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ChatController < ApplicationController
4
+ def index
5
+ @thread_id = session[:thread_id] ||= SecureRandom.uuid
6
+ thread = RobotLabThread.find_by(session_id: @thread_id)
7
+ @results = thread&.results&.to_a || []
8
+ end
9
+
10
+ def create
11
+ thread_id = session[:thread_id] ||= SecureRandom.uuid
12
+ message = params[:message].to_s.strip
13
+ return redirect_to root_path if message.empty?
14
+
15
+ # Persist user message so it appears in history on reload
16
+ thread = RobotLabThread.find_or_create_by_session_id(thread_id)
17
+ sequence = thread.results.maximum(:sequence_number).to_i + 1
18
+ thread.results.create!(
19
+ robot_name: "user",
20
+ sequence_number: sequence,
21
+ output_data: [{ role: "user", content: message }],
22
+ tool_calls_data: [],
23
+ stop_reason: "user_message",
24
+ checksum: Digest::SHA256.hexdigest(message)
25
+ )
26
+
27
+ RobotRunJob.perform_later(
28
+ robot_class: "ChatRobot",
29
+ message: message,
30
+ thread_id: thread_id
31
+ )
32
+
33
+ respond_to do |format|
34
+ format.turbo_stream do
35
+ render turbo_stream: [
36
+ turbo_stream.append("messages", partial: "chat/user_message", locals: { message: message }),
37
+ turbo_stream.replace("robot_status", "<div id=\"robot_status\"><span class=\"thinking\">Thinking...</span></div>"),
38
+ turbo_stream.update("robot_response", ""),
39
+ turbo_stream.update("robot_tools", ""),
40
+ turbo_stream.update("robot_errors", "")
41
+ ]
42
+ end
43
+ format.html { redirect_to root_path }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationJob < ActiveJob::Base
4
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generic background job for executing any robot asynchronously.
4
+ #
5
+ # Inherits from RobotLab::Job — Turbo Stream wiring, thread persistence,
6
+ # and completion/error broadcasting are all handled by the base class.
7
+ #
8
+ # Pass robot_class: at enqueue time to select which robot to run.
9
+ #
10
+ # @example Enqueue from a controller
11
+ # RobotRunJob.perform_later(
12
+ # robot_class: "ChatRobot",
13
+ # message: params[:message],
14
+ # thread_id: session_id
15
+ # )
16
+ #
17
+ class RobotRunJob < RobotLab::Job
18
+ queue_as :default
19
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RobotLabResult < ApplicationRecord
4
+ belongs_to :thread,
5
+ class_name: "RobotLabThread",
6
+ foreign_key: :session_id,
7
+ primary_key: :session_id
8
+
9
+ validates :session_id, presence: true
10
+ validates :robot_name, presence: true
11
+ validates :sequence_number, presence: true,
12
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
13
+
14
+ default_scope { order(sequence_number: :asc) }
15
+
16
+ def output_messages
17
+ (output_data || []).map do |data|
18
+ RobotLab::Message.from_hash(data.symbolize_keys)
19
+ end
20
+ end
21
+
22
+ def tool_call_messages
23
+ (tool_calls_data || []).map do |data|
24
+ RobotLab::Message.from_hash(data.symbolize_keys)
25
+ end
26
+ end
27
+
28
+ def to_robot_result
29
+ RobotLab::RobotResult.new(
30
+ robot_name: robot_name,
31
+ output: output_messages,
32
+ tool_calls: tool_call_messages,
33
+ stop_reason: stop_reason
34
+ )
35
+ end
36
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RobotLabThread < ApplicationRecord
4
+ has_many :results,
5
+ class_name: "RobotLabResult",
6
+ foreign_key: :session_id,
7
+ primary_key: :session_id,
8
+ dependent: :destroy
9
+
10
+ validates :session_id, presence: true, uniqueness: true
11
+
12
+ def self.find_or_create_by_session_id(id)
13
+ find_or_create_by(session_id: id)
14
+ end
15
+
16
+ def last_result
17
+ results.order(sequence_number: :desc).first
18
+ end
19
+
20
+ def message_count
21
+ results.count
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ChatRobot
4
+ SYSTEM_PROMPT = "You are a friendly assistant. Be concise."
5
+
6
+ def self.build(**options)
7
+ RobotLab.build(
8
+ name: "chat",
9
+ system_prompt: SYSTEM_PROMPT,
10
+ local_tools: [TimeTool],
11
+ **options
12
+ )
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TimeTool < RobotLab::Tool
4
+ description "Get the current date and time"
5
+
6
+ def execute
7
+ Time.current.strftime("%Y-%m-%d %H:%M:%S %Z")
8
+ end
9
+ end
@@ -0,0 +1 @@
1
+ <div class="message user"><%= message %></div>
@@ -0,0 +1,67 @@
1
+ <h1>RobotLab Chat</h1>
2
+
3
+ <%= turbo_stream_from "robot_lab_thread_#{@thread_id}" %>
4
+
5
+ <%= form_with url: chat_path, method: :post, class: "chat-form" do |f| %>
6
+ <%= f.text_field :message, placeholder: "Type a message...", autofocus: true, autocomplete: "off" %>
7
+ <%= f.submit "Send" %>
8
+ <% end %>
9
+
10
+ <div id="chat-scroll">
11
+ <div id="messages">
12
+ <% @results.each do |result| %>
13
+ <% if result.robot_name == "user" %>
14
+ <% result.output_messages.each do |msg| %>
15
+ <div class="message user"><%= msg.content %></div>
16
+ <% end %>
17
+ <% else %>
18
+ <% result.output_messages.each do |msg| %>
19
+ <div class="message assistant"><%= msg.content %></div>
20
+ <% end %>
21
+ <% unless result.tool_call_messages.empty? %>
22
+ <div>
23
+ <% result.tool_call_messages.each do |tc| %>
24
+ <span class="tool-badge">Used: <%= tc.content %></span>
25
+ <% end %>
26
+ </div>
27
+ <% end %>
28
+ <% end %>
29
+ <% end %>
30
+ </div>
31
+
32
+ <div id="robot_tools"></div>
33
+ <div id="robot_response" class="message assistant" style="display:none;"></div>
34
+ <div id="robot_errors"></div>
35
+ <div id="robot_status"></div>
36
+ </div>
37
+
38
+ <script>
39
+ const scrollContainer = document.getElementById('chat-scroll');
40
+ const responseEl = document.getElementById('robot_response');
41
+
42
+ function scrollToBottom() {
43
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
44
+ }
45
+
46
+ // Auto-scroll on any mutation inside the scroll container
47
+ const observer = new MutationObserver(function() {
48
+ if (responseEl.textContent.trim().length > 0) {
49
+ responseEl.style.display = 'block';
50
+ }
51
+ scrollToBottom();
52
+ });
53
+
54
+ observer.observe(scrollContainer, { childList: true, characterData: true, subtree: true });
55
+
56
+ // Clear the input field after form submission
57
+ document.addEventListener('turbo:submit-end', function(event) {
58
+ const input = event.target.querySelector('input[name="message"]');
59
+ if (input) {
60
+ input.value = '';
61
+ input.focus();
62
+ }
63
+ });
64
+
65
+ // Scroll to bottom on initial load if there's history
66
+ scrollToBottom();
67
+ </script>
@@ -0,0 +1,49 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>RobotLab Rails Demo</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= action_cable_meta_tag %>
7
+ <script type="importmap">
8
+ {
9
+ "imports": {
10
+ "@hotwired/turbo": "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.12/+esm",
11
+ "@hotwired/turbo-rails": "https://cdn.jsdelivr.net/npm/@hotwired/turbo-rails@8.0.12/+esm",
12
+ "@rails/actioncable": "https://cdn.jsdelivr.net/npm/@rails/actioncable@8.0.200/+esm"
13
+ }
14
+ }
15
+ </script>
16
+ <script type="module">
17
+ import "@hotwired/turbo-rails"
18
+ </script>
19
+ <style>
20
+ * { box-sizing: border-box; margin: 0; padding: 0; }
21
+ body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; align-items: center; overflow: hidden; }
22
+ .container { max-width: 720px; width: 100%; padding: 2rem 1rem; display: flex; flex-direction: column; height: 100%; overflow: hidden; }
23
+ #chat-scroll { flex: 1; overflow-y: auto; scroll-behavior: smooth; padding-right: 0.25rem; }
24
+ #chat-scroll::-webkit-scrollbar { width: 6px; }
25
+ #chat-scroll::-webkit-scrollbar-track { background: transparent; }
26
+ #chat-scroll::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
27
+ h1 { color: #7c83ff; margin-bottom: 1.5rem; font-size: 1.5rem; }
28
+ .chat-form { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; }
29
+ .chat-form input[type="text"] { flex: 1; padding: 0.75rem; border-radius: 8px; border: 1px solid #333; background: #16213e; color: #e0e0e0; font-size: 1rem; }
30
+ .chat-form input[type="text"]:focus { outline: none; border-color: #7c83ff; }
31
+ .chat-form button { padding: 0.75rem 1.5rem; border-radius: 8px; border: none; background: #7c83ff; color: #fff; font-size: 1rem; cursor: pointer; }
32
+ .chat-form button:hover { background: #6a70e0; }
33
+ .message { padding: 0.75rem 1rem; margin-bottom: 0.5rem; border-radius: 8px; line-height: 1.5; white-space: pre-wrap; }
34
+ .message.user { background: #1f4068; margin-left: 4rem; }
35
+ .message.assistant { background: #16213e; margin-right: 4rem; border: 1px solid #2a2a4a; }
36
+ .tool-badge { display: inline-block; background: #2d1b69; color: #b8b0ff; padding: 0.2rem 0.6rem; border-radius: 4px; font-size: 0.8rem; margin: 0.25rem 0.25rem 0.25rem 0; }
37
+ .thinking { color: #7c83ff; font-style: italic; }
38
+ .complete { color: #4ade80; font-size: 0.85rem; }
39
+ .error { color: #f87171; background: #2d1320; padding: 0.5rem 0.75rem; border-radius: 6px; margin-bottom: 0.5rem; }
40
+ #robot_response { white-space: pre-wrap; }
41
+ .history-label { color: #666; font-size: 0.75rem; text-transform: uppercase; margin-bottom: 0.75rem; letter-spacing: 0.05em; }
42
+ </style>
43
+ </head>
44
+ <body>
45
+ <div class="container">
46
+ <%= yield %>
47
+ </div>
48
+ </body>
49
+ </html>
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ cd "$(dirname "$0")/.."
5
+
6
+ echo "Starting Puma on http://localhost:3000 ..."
7
+ bundle exec rails server -p 3000
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ APP_PATH = File.expand_path("../config/application", __dir__)
5
+ require_relative "../config/application"
6
+ require "rails/commands"
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ cd "$(dirname "$0")/.."
5
+
6
+ echo "==> Installing gems..."
7
+ bundle install
8
+
9
+ echo "==> Creating database..."
10
+ bundle exec rails db:create
11
+
12
+ echo "==> Running migrations..."
13
+ bundle exec rails db:migrate
14
+
15
+ echo "==> Done! Run bin/dev to start the server."
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ require "rails"
6
+ require "active_record/railtie"
7
+ require "active_job/railtie"
8
+ require "action_controller/railtie"
9
+ require "action_view/railtie"
10
+ require "action_cable/engine"
11
+
12
+ Bundler.require(*Rails.groups)
13
+
14
+ module RobotLabDemo
15
+ class Application < Rails::Application
16
+ config.load_defaults 8.0
17
+
18
+ config.eager_load = false
19
+ config.consider_all_requests_local = true
20
+ config.secret_key_base = "demo-secret-key-for-development-only"
21
+
22
+ # Use async adapter for jobs — no external process needed
23
+ config.active_job.queue_adapter = :async
24
+
25
+ # Autoload app/robots and app/tools
26
+ config.autoload_paths << root.join("app", "robots")
27
+ config.autoload_paths << root.join("app", "tools")
28
+
29
+ # Disable unused middleware
30
+ config.api_only = false
31
+ config.hosts.clear
32
+ end
33
+ end
@@ -0,0 +1,2 @@
1
+ development:
2
+ adapter: async
@@ -0,0 +1,5 @@
1
+ development:
2
+ adapter: sqlite3
3
+ database: db/development.sqlite3
4
+ pool: 5
5
+ timeout: 5000
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "application"
4
+ Rails.application.initialize!
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RobotLab.config.logger = Rails.logger
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ root "chat#index"
5
+ post "chat", to: "chat#create"
6
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config/environment"
4
+ run Rails.application
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRobotLabTables < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :robot_lab_threads do |t|
6
+ t.string :session_id, null: false, index: { unique: true }
7
+ t.text :initial_input
8
+ t.json :input_metadata, default: {}
9
+ t.json :state_data, default: {}
10
+ t.text :last_user_message
11
+ t.datetime :last_user_message_at
12
+
13
+ t.timestamps
14
+ end
15
+
16
+ create_table :robot_lab_results do |t|
17
+ t.string :session_id, null: false, index: true
18
+ t.string :robot_name, null: false
19
+ t.integer :sequence_number, null: false, default: 0
20
+ t.json :output_data, default: []
21
+ t.json :tool_calls_data, default: []
22
+ t.string :stop_reason
23
+ t.string :checksum
24
+
25
+ t.timestamps
26
+ end
27
+
28
+ add_index :robot_lab_results, [:session_id, :sequence_number]
29
+ add_foreign_key :robot_lab_results, :robot_lab_threads,
30
+ column: :session_id, primary_key: :session_id
31
+ end
32
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module RobotLab
7
+ module Generators
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ include ::Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ class_option :skip_migration, type: :boolean, default: false,
14
+ desc: "Skip database migration generation"
15
+ class_option :skip_job, type: :boolean, default: false,
16
+ desc: "Skip background job generation"
17
+
18
+ def self.next_migration_number(dirname)
19
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
20
+ end
21
+
22
+ def create_initializer
23
+ template "initializer.rb.tt", "config/initializers/robot_lab.rb"
24
+ end
25
+
26
+ def create_migration
27
+ return if options[:skip_migration]
28
+
29
+ migration_template "migration.rb.tt", "db/migrate/create_robot_lab_tables.rb"
30
+ end
31
+
32
+ def create_models
33
+ return if options[:skip_migration]
34
+
35
+ template "thread_model.rb.tt", "app/models/robot_lab_thread.rb"
36
+ template "result_model.rb.tt", "app/models/robot_lab_result.rb"
37
+ end
38
+
39
+ def create_job
40
+ return if options[:skip_job]
41
+
42
+ template "job.rb.tt", "app/jobs/robot_run_job.rb"
43
+ end
44
+
45
+ def create_directories
46
+ empty_directory "app/robots"
47
+ empty_directory "app/tools"
48
+ end
49
+
50
+ def display_post_install
51
+ say ""
52
+ say "RobotLab installed successfully!", :green
53
+ say ""
54
+ say "Next steps:"
55
+ say " 1. Run migrations: rails db:migrate"
56
+ say " 2. Configure your LLM API keys in config/initializers/robot_lab.rb"
57
+ say " 3. Generate your first robot: rails g robot_lab:robot MyRobot"
58
+ say " 4. Enqueue robot runs via RobotRunJob (app/jobs/robot_run_job.rb)" unless options[:skip_job]
59
+ say ""
60
+ end
61
+ end
62
+ end
63
+ end