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
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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-05-07
4
+
5
+ - Initial release
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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -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)