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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 93f8a8d257c9dfe599fde397b4be1cc979df53eed8d38a3adca987e493852311
|
|
4
|
+
data.tar.gz: a41afb0b4132c1c245a10f44d3b5d9dd40ced45ad264e0a7e08c342cf9fb5711
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d84d9c25a0379bca316619e0268c9f727228572c09048cb2717591acf145f1459c4292617f3ba836d756a9bd0f79b4922fdec43812cf3d111ab57d8dc591d945
|
|
7
|
+
data.tar.gz: e908c4bee35578e1ecd313b126ea87b7cc3719eba286cbe01ed11000fdd65dc09b41ba49fc069da64137d783d8c19076ae5dac4dd5418b8aac7b8b89c32b31ad
|
data/.envrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export RR=`pwd`
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Deploy Documentation to GitHub Pages
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
- develop
|
|
7
|
+
paths:
|
|
8
|
+
- "docs/**"
|
|
9
|
+
- "mkdocs.yml"
|
|
10
|
+
- ".github/workflows/deploy-github-pages.yml"
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
pages: write
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
deploy:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout code
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
with:
|
|
25
|
+
fetch-depth: 0
|
|
26
|
+
|
|
27
|
+
- name: Setup Python
|
|
28
|
+
uses: actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: 3.x
|
|
31
|
+
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: |
|
|
34
|
+
pip install mkdocs
|
|
35
|
+
pip install mkdocs-material
|
|
36
|
+
pip install mkdocs-macros-plugin
|
|
37
|
+
pip install mike
|
|
38
|
+
|
|
39
|
+
- name: Configure Git
|
|
40
|
+
run: |
|
|
41
|
+
git config --local user.email "action@github.com"
|
|
42
|
+
git config --local user.name "GitHub Action"
|
|
43
|
+
|
|
44
|
+
- name: Build MkDocs site
|
|
45
|
+
run: mkdocs build
|
|
46
|
+
|
|
47
|
+
- name: Deploy to GitHub Pages
|
|
48
|
+
uses: peaceiris/actions-gh-pages@v4
|
|
49
|
+
with:
|
|
50
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
51
|
+
publish_dir: ./site
|
|
52
|
+
keep_files: true
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dewayne VanHoozer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
This creates:
|
|
32
|
+
|
|
33
|
+
- `config/initializers/robot_lab.rb` — configuration
|
|
34
|
+
- `app/robots/` — directory for your robots
|
|
35
|
+
- Database tables for conversation history
|
|
36
|
+
|
|
37
|
+
## Background Jobs
|
|
38
|
+
|
|
39
|
+
`RobotLab::Job` is an `ActiveJob::Base` subclass that handles the full robot-run lifecycle: robot class resolution, Turbo Stream wiring, thread-record persistence, and completion/error broadcasting.
|
|
40
|
+
|
|
41
|
+
**Generic job** (robot class supplied at enqueue time):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
rails generate robot_lab:install # creates app/jobs/robot_run_job.rb
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# app/jobs/robot_run_job.rb (generated)
|
|
49
|
+
class RobotRunJob < RobotLab::Job
|
|
50
|
+
queue_as :default
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Enqueue from a controller:
|
|
54
|
+
RobotRunJob.perform_later(
|
|
55
|
+
robot_class: "SupportRobot",
|
|
56
|
+
message: params[:message],
|
|
57
|
+
thread_id: session_id
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Dedicated job** (robot class bound at the class level via DSL):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
rails generate robot_lab:job Support # binds to SupportRobot, queue: default
|
|
65
|
+
rails generate robot_lab:job Support --queue ai # custom queue
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# app/jobs/support_job.rb (generated)
|
|
70
|
+
class SupportJob < RobotLab::Job
|
|
71
|
+
queue_as :default
|
|
72
|
+
robot_class SupportRobot
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Enqueue (no robot_class: needed):
|
|
76
|
+
SupportJob.perform_later(message: params[:message], thread_id: session_id)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Omitting `thread_id` runs the robot in fire-and-forget mode — no persistence, no broadcasting.
|
|
80
|
+
|
|
81
|
+
## Turbo Stream Streaming
|
|
82
|
+
|
|
83
|
+
When `thread_id` is provided and [turbo-rails](https://github.com/hotwired/turbo-rails) is installed, `RobotLab::Job` automatically:
|
|
84
|
+
|
|
85
|
+
- Wires `on_content` / `on_tool_call` Turbo Stream callbacks so the UI updates in real time
|
|
86
|
+
- Broadcasts a **completion** event to `"robot_lab_thread_#{thread_id}"` when the run finishes
|
|
87
|
+
- Broadcasts an **error** event (HTML-escaped) if the job raises
|
|
88
|
+
|
|
89
|
+
In your view:
|
|
90
|
+
|
|
91
|
+
```erb
|
|
92
|
+
<%= turbo_stream_from "robot_lab_thread_#{session[:id]}" %>
|
|
93
|
+
<div id="robot_response"></div>
|
|
94
|
+
<div id="robot_status"></div>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Links
|
|
98
|
+
|
|
99
|
+
- [Rails Integration Guide](https://madbomber.github.io/robot_lab-rails/guides/rails-integration)
|
|
100
|
+
- [Rails Application Example](https://madbomber.github.io/robot_lab-rails/examples/rails-application)
|
|
101
|
+
- [RobotLab Core](https://github.com/MadBomber/robot_lab)
|
|
102
|
+
- [RubyGems](https://rubygems.org/gems/robot_lab-rails)
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT License - Copyright (c) 2025 Dewayne VanHoozer
|
|
107
|
+
|
|
108
|
+
## Contributing
|
|
109
|
+
|
|
110
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/MadBomber/robot_lab-rails.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# Rails Application
|
|
2
|
+
|
|
3
|
+
Full Rails integration with Action Cable and background jobs.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This example demonstrates integrating RobotLab into a Rails application with real-time streaming via Action Cable, background job processing, and persistent conversation history.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
### 1. Add to Gemfile
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
# Gemfile
|
|
15
|
+
gem "robot_lab"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### 2. Run Generator
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
rails generate robot_lab:install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This creates:
|
|
25
|
+
|
|
26
|
+
- `config/initializers/robot_lab.rb`
|
|
27
|
+
- `app/robots/` directory
|
|
28
|
+
- `app/tools/` directory
|
|
29
|
+
- Database migrations for conversation history
|
|
30
|
+
|
|
31
|
+
### 3. Run Migrations
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
rails db:migrate
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
RobotLab uses MywayConfig for configuration. There is no `RobotLab.configure` block. Instead, configuration is loaded automatically from multiple sources in priority order:
|
|
40
|
+
|
|
41
|
+
1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
|
|
42
|
+
2. Environment-specific overrides (development, test, production)
|
|
43
|
+
3. XDG user config (`~/.config/robot_lab/config.yml`)
|
|
44
|
+
4. Project config (`./config/robot_lab.yml`)
|
|
45
|
+
5. Environment variables (`ROBOT_LAB_*` prefix)
|
|
46
|
+
|
|
47
|
+
### Config File
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
# config/robot_lab.yml
|
|
51
|
+
defaults:
|
|
52
|
+
ruby_llm:
|
|
53
|
+
model: claude-sonnet-4
|
|
54
|
+
anthropic_api_key: <%= ENV['ANTHROPIC_API_KEY'] %>
|
|
55
|
+
|
|
56
|
+
development:
|
|
57
|
+
ruby_llm:
|
|
58
|
+
log_level: :debug
|
|
59
|
+
|
|
60
|
+
production:
|
|
61
|
+
ruby_llm:
|
|
62
|
+
request_timeout: 180
|
|
63
|
+
max_retries: 5
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Environment Variables
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Provider API keys
|
|
70
|
+
ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
|
|
71
|
+
ROBOT_LAB_RUBY_LLM__OPENAI_API_KEY=sk-...
|
|
72
|
+
|
|
73
|
+
# Model configuration
|
|
74
|
+
ROBOT_LAB_RUBY_LLM__MODEL=claude-sonnet-4
|
|
75
|
+
ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=120
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Accessing Configuration
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
# Read configuration values at runtime
|
|
82
|
+
RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
|
|
83
|
+
RobotLab.config.ruby_llm.request_timeout #=> 120
|
|
84
|
+
|
|
85
|
+
# The logger defaults to Rails.logger when running in Rails
|
|
86
|
+
RobotLab.config.logger #=> Rails.logger
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Rails Engine and Railtie
|
|
90
|
+
|
|
91
|
+
RobotLab provides both a Rails Engine (`RobotLab::RailsIntegration::Engine`) and a Railtie (`RobotLab::RailsIntegration::Railtie`). These are loaded automatically when Rails is detected. The Engine isolates the RobotLab namespace and adds `app/robots` and `app/tools` to the autoload paths. The Railtie loads rake tasks and generators.
|
|
92
|
+
|
|
93
|
+
## Models
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
# app/models/conversation_thread.rb
|
|
97
|
+
class ConversationThread < ApplicationRecord
|
|
98
|
+
belongs_to :user
|
|
99
|
+
has_many :messages, class_name: "ConversationMessage", dependent: :destroy
|
|
100
|
+
|
|
101
|
+
validates :external_id, presence: true, uniqueness: true
|
|
102
|
+
|
|
103
|
+
def self.find_or_create_for(user:, external_id: nil)
|
|
104
|
+
external_id ||= SecureRandom.uuid
|
|
105
|
+
find_or_create_by!(user: user, external_id: external_id)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# app/models/conversation_message.rb
|
|
110
|
+
class ConversationMessage < ApplicationRecord
|
|
111
|
+
belongs_to :thread, class_name: "ConversationThread"
|
|
112
|
+
|
|
113
|
+
validates :role, presence: true
|
|
114
|
+
validates :content, presence: true
|
|
115
|
+
|
|
116
|
+
scope :ordered, -> { order(:position) }
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Robot Definitions
|
|
121
|
+
|
|
122
|
+
Robots are built using `RobotLab.build` with named parameters. Tools are Ruby classes that inherit from `RubyLLM::Tool`.
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# app/tools/get_user_info_tool.rb
|
|
126
|
+
class GetUserInfoTool < RubyLLM::Tool
|
|
127
|
+
description "Get information about the current user"
|
|
128
|
+
|
|
129
|
+
param :user_id, type: :integer, desc: "The user ID to look up"
|
|
130
|
+
|
|
131
|
+
def execute(user_id:)
|
|
132
|
+
user = User.find(user_id)
|
|
133
|
+
{
|
|
134
|
+
name: user.name,
|
|
135
|
+
email: user.email,
|
|
136
|
+
plan: user.subscription&.plan || "free",
|
|
137
|
+
member_since: user.created_at.to_date.to_s
|
|
138
|
+
}
|
|
139
|
+
rescue ActiveRecord::RecordNotFound
|
|
140
|
+
{ error: "User not found" }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# app/tools/get_orders_tool.rb
|
|
145
|
+
class GetOrdersTool < RubyLLM::Tool
|
|
146
|
+
description "Get user's recent orders"
|
|
147
|
+
|
|
148
|
+
param :user_id, type: :integer, desc: "The user ID"
|
|
149
|
+
param :limit, type: :integer, desc: "Number of orders to return", default: 5
|
|
150
|
+
|
|
151
|
+
def execute(user_id:, limit: 5)
|
|
152
|
+
orders = Order.where(user_id: user_id)
|
|
153
|
+
.order(created_at: :desc)
|
|
154
|
+
.limit(limit)
|
|
155
|
+
|
|
156
|
+
orders.map do |order|
|
|
157
|
+
{
|
|
158
|
+
id: order.external_id,
|
|
159
|
+
status: order.status,
|
|
160
|
+
total: order.total.to_f,
|
|
161
|
+
created_at: order.created_at.iso8601
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# app/tools/create_ticket_tool.rb
|
|
168
|
+
class CreateTicketTool < RubyLLM::Tool
|
|
169
|
+
description "Create a support ticket"
|
|
170
|
+
|
|
171
|
+
param :user_id, type: :integer, desc: "The user ID"
|
|
172
|
+
param :subject, type: :string, desc: "Ticket subject"
|
|
173
|
+
param :description, type: :string, desc: "Ticket description"
|
|
174
|
+
param :priority, type: :string, desc: "Priority level", enum: %w[low medium high]
|
|
175
|
+
|
|
176
|
+
def execute(user_id:, subject:, description:, priority: "medium")
|
|
177
|
+
ticket = SupportTicket.create!(
|
|
178
|
+
user_id: user_id,
|
|
179
|
+
subject: subject,
|
|
180
|
+
description: description,
|
|
181
|
+
priority: priority
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
{
|
|
185
|
+
success: true,
|
|
186
|
+
ticket_id: ticket.external_id,
|
|
187
|
+
message: "Ticket created successfully"
|
|
188
|
+
}
|
|
189
|
+
rescue => e
|
|
190
|
+
{ success: false, error: e.message }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# app/robots/support_robot.rb
|
|
197
|
+
class SupportRobot
|
|
198
|
+
def self.build(user_id:)
|
|
199
|
+
RobotLab.build(
|
|
200
|
+
name: "support",
|
|
201
|
+
system_prompt: <<~PROMPT,
|
|
202
|
+
You are a helpful customer support assistant for our company.
|
|
203
|
+
Be friendly, professional, and thorough in your responses.
|
|
204
|
+
If you need to look up information, use the available tools.
|
|
205
|
+
The current user ID is #{user_id}.
|
|
206
|
+
PROMPT
|
|
207
|
+
local_tools: [GetUserInfoTool, GetOrdersTool, CreateTicketTool]
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Network Configuration
|
|
214
|
+
|
|
215
|
+
Networks use `create_network` with a block DSL that defines tasks and their dependencies:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
# app/robots/support_network.rb
|
|
219
|
+
class SupportNetwork
|
|
220
|
+
def self.build(user_id:)
|
|
221
|
+
support = SupportRobot.build(user_id: user_id)
|
|
222
|
+
|
|
223
|
+
RobotLab.create_network(name: "support_network") do
|
|
224
|
+
task :support, support, depends_on: :none
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Service Object
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
# app/services/chat_service.rb
|
|
234
|
+
class ChatService
|
|
235
|
+
def initialize(user:, thread_id: nil)
|
|
236
|
+
@user = user
|
|
237
|
+
@thread_id = thread_id
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def call(message:)
|
|
241
|
+
robot = SupportRobot.build(user_id: @user.id)
|
|
242
|
+
result = robot.run(message)
|
|
243
|
+
|
|
244
|
+
{
|
|
245
|
+
response: result.last_text_content,
|
|
246
|
+
has_tool_calls: result.has_tool_calls?
|
|
247
|
+
}
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Controller
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
# app/controllers/api/chats_controller.rb
|
|
256
|
+
module Api
|
|
257
|
+
class ChatsController < ApplicationController
|
|
258
|
+
before_action :authenticate_user!
|
|
259
|
+
|
|
260
|
+
def create
|
|
261
|
+
service = ChatService.new(
|
|
262
|
+
user: current_user,
|
|
263
|
+
thread_id: params[:thread_id]
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
result = service.call(message: params[:message])
|
|
267
|
+
|
|
268
|
+
render json: {
|
|
269
|
+
response: result[:response]
|
|
270
|
+
}
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Action Cable Integration
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
# app/channels/chat_channel.rb
|
|
280
|
+
class ChatChannel < ApplicationCable::Channel
|
|
281
|
+
def subscribed
|
|
282
|
+
stream_for current_user
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def receive(data)
|
|
286
|
+
ChatJob.perform_later(
|
|
287
|
+
user_id: current_user.id,
|
|
288
|
+
thread_id: data["thread_id"],
|
|
289
|
+
message: data["message"]
|
|
290
|
+
)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# app/jobs/chat_job.rb
|
|
295
|
+
class ChatJob < ApplicationJob
|
|
296
|
+
queue_as :default
|
|
297
|
+
|
|
298
|
+
def perform(user_id:, thread_id:, message:)
|
|
299
|
+
user = User.find(user_id)
|
|
300
|
+
robot = SupportRobot.build(user_id: user.id)
|
|
301
|
+
|
|
302
|
+
result = robot.run(message)
|
|
303
|
+
|
|
304
|
+
ChatChannel.broadcast_to(
|
|
305
|
+
user,
|
|
306
|
+
type: "complete",
|
|
307
|
+
content: result.last_text_content
|
|
308
|
+
)
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Frontend (Stimulus)
|
|
314
|
+
|
|
315
|
+
```javascript
|
|
316
|
+
// app/javascript/controllers/chat_controller.js
|
|
317
|
+
import { Controller } from "@hotwired/stimulus"
|
|
318
|
+
import { createConsumer } from "@rails/actioncable"
|
|
319
|
+
|
|
320
|
+
export default class extends Controller {
|
|
321
|
+
static targets = ["messages", "input", "response"]
|
|
322
|
+
|
|
323
|
+
connect() {
|
|
324
|
+
this.consumer = createConsumer()
|
|
325
|
+
this.channel = this.consumer.subscriptions.create("ChatChannel", {
|
|
326
|
+
received: (data) => this.handleMessage(data)
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
disconnect() {
|
|
331
|
+
this.channel?.unsubscribe()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
send() {
|
|
335
|
+
const message = this.inputTarget.value.trim()
|
|
336
|
+
if (!message) return
|
|
337
|
+
|
|
338
|
+
this.appendMessage("user", message)
|
|
339
|
+
this.inputTarget.value = ""
|
|
340
|
+
|
|
341
|
+
// Create response container
|
|
342
|
+
this.currentResponse = document.createElement("div")
|
|
343
|
+
this.currentResponse.className = "message assistant"
|
|
344
|
+
this.messagesTarget.appendChild(this.currentResponse)
|
|
345
|
+
|
|
346
|
+
this.channel.send({
|
|
347
|
+
message: message,
|
|
348
|
+
thread_id: this.threadId
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
handleMessage(data) {
|
|
353
|
+
switch (data.type) {
|
|
354
|
+
case "complete":
|
|
355
|
+
this.currentResponse.textContent = data.content
|
|
356
|
+
break
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
appendMessage(role, content) {
|
|
361
|
+
const div = document.createElement("div")
|
|
362
|
+
div.className = `message ${role}`
|
|
363
|
+
div.textContent = content
|
|
364
|
+
this.messagesTarget.appendChild(div)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## View
|
|
370
|
+
|
|
371
|
+
```erb
|
|
372
|
+
<!-- app/views/chats/show.html.erb -->
|
|
373
|
+
<div data-controller="chat">
|
|
374
|
+
<div class="messages" data-chat-target="messages">
|
|
375
|
+
<!-- Messages appear here -->
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<form data-action="submit->chat#send">
|
|
379
|
+
<input type="text"
|
|
380
|
+
data-chat-target="input"
|
|
381
|
+
placeholder="Type a message..."
|
|
382
|
+
autocomplete="off">
|
|
383
|
+
<button type="submit">Send</button>
|
|
384
|
+
</form>
|
|
385
|
+
</div>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Running
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
# Install dependencies
|
|
392
|
+
bundle install
|
|
393
|
+
yarn install
|
|
394
|
+
|
|
395
|
+
# Setup database
|
|
396
|
+
rails db:migrate
|
|
397
|
+
|
|
398
|
+
# Set API key (or configure via config/robot_lab.yml)
|
|
399
|
+
export ANTHROPIC_API_KEY="your-key"
|
|
400
|
+
|
|
401
|
+
# Start server
|
|
402
|
+
bin/dev
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Key Concepts
|
|
406
|
+
|
|
407
|
+
1. **Robot Factory**: `RobotLab.build(name:, system_prompt:, local_tools:, ...)` creates robot instances
|
|
408
|
+
2. **MywayConfig**: Configuration via YAML files and environment variables, not a configure block
|
|
409
|
+
3. **`robot.run("message")`**: Send a message as a positional string argument
|
|
410
|
+
4. **`result.last_text_content`**: Extract the response text from a `RobotResult`
|
|
411
|
+
5. **Memory**: Robots have `robot.memory` for key-value storage; networks share memory
|
|
412
|
+
6. **Tools**: Ruby classes inheriting from `RubyLLM::Tool`, passed via `local_tools:`
|
|
413
|
+
7. **Action Cable**: Real-time streaming to browser
|
|
414
|
+
8. **Background Jobs**: Non-blocking processing
|
|
415
|
+
|
|
416
|
+
## See Also
|
|
417
|
+
|
|
418
|
+
- [Rails Integration Guide](../guides/rails-integration.md)
|
|
419
|
+
- [Streaming Guide](../guides/streaming.md)
|