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.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +110 -0
- data/Rakefile +8 -0
- data/docs/examples/rails-application.md +419 -0
- data/docs/guides/rails-integration.md +681 -0
- data/docs/index.md +75 -0
- data/examples/18_rails/.envrc +3 -0
- data/examples/18_rails/.gitignore +5 -0
- data/examples/18_rails/Gemfile +11 -0
- data/examples/18_rails/README.md +48 -0
- data/examples/18_rails/Rakefile +4 -0
- data/examples/18_rails/app/controllers/application_controller.rb +4 -0
- data/examples/18_rails/app/controllers/chat_controller.rb +46 -0
- data/examples/18_rails/app/jobs/application_job.rb +4 -0
- data/examples/18_rails/app/jobs/robot_run_job.rb +19 -0
- data/examples/18_rails/app/models/application_record.rb +5 -0
- data/examples/18_rails/app/models/robot_lab_result.rb +36 -0
- data/examples/18_rails/app/models/robot_lab_thread.rb +23 -0
- data/examples/18_rails/app/robots/chat_robot.rb +14 -0
- data/examples/18_rails/app/tools/time_tool.rb +9 -0
- data/examples/18_rails/app/views/chat/_user_message.html.erb +1 -0
- data/examples/18_rails/app/views/chat/index.html.erb +67 -0
- data/examples/18_rails/app/views/layouts/application.html.erb +49 -0
- data/examples/18_rails/bin/dev +7 -0
- data/examples/18_rails/bin/rails +6 -0
- data/examples/18_rails/bin/setup +15 -0
- data/examples/18_rails/config/application.rb +33 -0
- data/examples/18_rails/config/cable.yml +2 -0
- data/examples/18_rails/config/database.yml +5 -0
- data/examples/18_rails/config/environment.rb +4 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +3 -0
- data/examples/18_rails/config/routes.rb +6 -0
- data/examples/18_rails/config.ru +4 -0
- data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +32 -0
- data/lib/generators/robot_lab/install_generator.rb +63 -0
- data/lib/generators/robot_lab/job_generator.rb +28 -0
- data/lib/generators/robot_lab/robot_generator.rb +40 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +39 -0
- data/lib/generators/robot_lab/templates/job.rb.tt +21 -0
- data/lib/generators/robot_lab/templates/migration.rb.tt +32 -0
- data/lib/generators/robot_lab/templates/result_model.rb.tt +40 -0
- data/lib/generators/robot_lab/templates/robot.rb.tt +31 -0
- data/lib/generators/robot_lab/templates/robot_job.rb.tt +15 -0
- data/lib/generators/robot_lab/templates/robot_test.rb.tt +21 -0
- data/lib/generators/robot_lab/templates/routing_robot.rb.tt +42 -0
- data/lib/generators/robot_lab/templates/thread_model.rb.tt +27 -0
- data/lib/robot_lab/rails/version.rb +7 -0
- data/lib/robot_lab/rails.rb +10 -0
- data/lib/robot_lab/rails_integration/engine.rb +23 -0
- data/lib/robot_lab/rails_integration/job.rb +109 -0
- data/lib/robot_lab/rails_integration/railtie.rb +36 -0
- data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +42 -0
- data/mkdocs.yml +117 -0
- 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,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,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,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,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 @@
|
|
|
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,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,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
|