robot_lab 0.0.1 → 0.0.4

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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +9 -9
  3. data/.irbrc +6 -0
  4. data/CHANGELOG.md +90 -0
  5. data/README.md +203 -46
  6. data/Rakefile +70 -1
  7. data/docs/api/core/index.md +12 -0
  8. data/docs/api/core/robot.md +478 -130
  9. data/docs/api/core/tool.md +205 -209
  10. data/docs/api/history/active-record-adapter.md +174 -94
  11. data/docs/api/history/config.md +186 -93
  12. data/docs/api/history/index.md +57 -61
  13. data/docs/api/history/thread-manager.md +123 -73
  14. data/docs/api/mcp/client.md +119 -48
  15. data/docs/api/mcp/index.md +75 -60
  16. data/docs/api/mcp/server.md +120 -136
  17. data/docs/api/mcp/transports.md +172 -184
  18. data/docs/api/streaming/context.md +157 -74
  19. data/docs/api/streaming/events.md +114 -166
  20. data/docs/api/streaming/index.md +74 -72
  21. data/docs/architecture/core-concepts.md +361 -112
  22. data/docs/architecture/index.md +97 -59
  23. data/docs/architecture/message-flow.md +138 -129
  24. data/docs/architecture/network-orchestration.md +197 -50
  25. data/docs/architecture/robot-execution.md +199 -146
  26. data/docs/architecture/state-management.md +255 -187
  27. data/docs/concepts.md +312 -48
  28. data/docs/examples/basic-chat.md +89 -77
  29. data/docs/examples/index.md +222 -47
  30. data/docs/examples/mcp-server.md +207 -203
  31. data/docs/examples/multi-robot-network.md +129 -35
  32. data/docs/examples/rails-application.md +159 -160
  33. data/docs/examples/tool-usage.md +295 -204
  34. data/docs/getting-started/configuration.md +275 -162
  35. data/docs/getting-started/index.md +1 -1
  36. data/docs/getting-started/installation.md +22 -13
  37. data/docs/getting-started/quick-start.md +166 -121
  38. data/docs/guides/building-robots.md +417 -212
  39. data/docs/guides/creating-networks.md +94 -24
  40. data/docs/guides/mcp-integration.md +152 -113
  41. data/docs/guides/memory.md +220 -164
  42. data/docs/guides/streaming.md +80 -110
  43. data/docs/guides/using-tools.md +259 -212
  44. data/docs/index.md +50 -37
  45. data/examples/01_simple_robot.rb +6 -9
  46. data/examples/02_tools.rb +6 -9
  47. data/examples/03_network.rb +13 -14
  48. data/examples/04_mcp.rb +5 -8
  49. data/examples/05_streaming.rb +5 -8
  50. data/examples/06_prompt_templates.rb +42 -37
  51. data/examples/07_network_memory.rb +13 -14
  52. data/examples/08_llm_config.rb +140 -0
  53. data/examples/09_chaining.rb +223 -0
  54. data/examples/10_memory.rb +331 -0
  55. data/examples/11_network_introspection.rb +230 -0
  56. data/examples/12_message_bus.rb +74 -0
  57. data/examples/13_spawn.rb +90 -0
  58. data/examples/14_rusty_circuit/comic.rb +143 -0
  59. data/examples/14_rusty_circuit/display.rb +203 -0
  60. data/examples/14_rusty_circuit/heckler.rb +57 -0
  61. data/examples/14_rusty_circuit/open_mic.rb +121 -0
  62. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
  63. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
  64. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
  65. data/examples/14_rusty_circuit/scout.rb +173 -0
  66. data/examples/14_rusty_circuit/scout_notes.md +89 -0
  67. data/examples/14_rusty_circuit/show.log +234 -0
  68. data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
  69. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
  70. data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
  71. data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
  72. data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
  73. data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
  74. data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
  75. data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
  76. data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
  77. data/examples/15_memory_network_and_bus/output/memory.json +13 -0
  78. data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
  79. data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
  80. data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
  81. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
  82. data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
  83. data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
  84. data/examples/README.md +197 -0
  85. data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
  86. data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
  87. data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
  88. data/examples/prompts/comedian.md +6 -0
  89. data/examples/prompts/comedy_critic.md +10 -0
  90. data/examples/prompts/configurable.md +9 -0
  91. data/examples/prompts/dispatcher.md +12 -0
  92. data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
  93. data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
  94. data/examples/prompts/frontmatter_mcp_test.md +9 -0
  95. data/examples/prompts/frontmatter_named_test.md +5 -0
  96. data/examples/prompts/frontmatter_tools_test.md +6 -0
  97. data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
  98. data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
  99. data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
  100. data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
  101. data/examples/prompts/llm_config_demo.md +20 -0
  102. data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
  103. data/examples/prompts/os_advocate.md +13 -0
  104. data/examples/prompts/os_chief.md +13 -0
  105. data/examples/prompts/os_editor.md +13 -0
  106. data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
  107. data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
  108. data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
  109. data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
  110. data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
  111. data/lib/generators/robot_lab/templates/initializer.rb.tt +1 -1
  112. data/lib/robot_lab/adapters/openai.rb +2 -1
  113. data/lib/robot_lab/ask_user.rb +75 -0
  114. data/lib/robot_lab/config/defaults.yml +121 -0
  115. data/lib/robot_lab/config.rb +183 -0
  116. data/lib/robot_lab/error.rb +6 -0
  117. data/lib/robot_lab/mcp/client.rb +1 -1
  118. data/lib/robot_lab/memory.rb +2 -2
  119. data/lib/robot_lab/robot.rb +523 -249
  120. data/lib/robot_lab/robot_message.rb +44 -0
  121. data/lib/robot_lab/robot_result.rb +1 -0
  122. data/lib/robot_lab/robotic_model.rb +1 -1
  123. data/lib/robot_lab/streaming/context.rb +1 -1
  124. data/lib/robot_lab/tool.rb +108 -172
  125. data/lib/robot_lab/tool_config.rb +1 -1
  126. data/lib/robot_lab/tool_manifest.rb +2 -18
  127. data/lib/robot_lab/version.rb +1 -1
  128. data/lib/robot_lab.rb +66 -55
  129. metadata +107 -116
  130. data/examples/prompts/assistant/user.txt.erb +0 -1
  131. data/examples/prompts/billing/user.txt.erb +0 -1
  132. data/examples/prompts/classifier/user.txt.erb +0 -1
  133. data/examples/prompts/entity_extractor/user.txt.erb +0 -3
  134. data/examples/prompts/escalation/user.txt.erb +0 -34
  135. data/examples/prompts/general/user.txt.erb +0 -1
  136. data/examples/prompts/github_assistant/user.txt.erb +0 -1
  137. data/examples/prompts/helper/user.txt.erb +0 -1
  138. data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
  139. data/examples/prompts/order_support/user.txt.erb +0 -22
  140. data/examples/prompts/product_support/user.txt.erb +0 -32
  141. data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
  142. data/examples/prompts/synthesizer/user.txt.erb +0 -15
  143. data/examples/prompts/technical/user.txt.erb +0 -1
  144. data/examples/prompts/triage/user.txt.erb +0 -17
  145. data/lib/robot_lab/configuration.rb +0 -143
@@ -25,7 +25,8 @@ This creates:
25
25
 
26
26
  - `config/initializers/robot_lab.rb`
27
27
  - `app/robots/` directory
28
- - Database migrations for history
28
+ - `app/tools/` directory
29
+ - Database migrations for conversation history
29
30
 
30
31
  ### 3. Run Migrations
31
32
 
@@ -35,17 +36,60 @@ rails db:migrate
35
36
 
36
37
  ## Configuration
37
38
 
38
- ```ruby
39
- # config/initializers/robot_lab.rb
39
+ RobotLab uses MywayConfig for configuration. There is no `RobotLab.configure` block. Instead, configuration is loaded automatically from multiple sources in priority order:
40
40
 
41
- RobotLab.configure do |config|
42
- config.default_model = ENV.fetch("LLM_MODEL", "claude-sonnet-4")
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)
43
46
 
44
- # Enable logging in development
45
- config.logger = Rails.logger if Rails.env.development?
46
- end
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
47
64
  ```
48
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::Rails::Engine`) and a Railtie (`RobotLab::Rails::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
+
49
93
  ## Models
50
94
 
51
95
  ```ruby
@@ -70,116 +114,114 @@ class ConversationMessage < ApplicationRecord
70
114
  validates :content, presence: true
71
115
 
72
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)
73
155
 
74
- def to_robot_result
75
- RobotLab::RobotResult.from_hash(
76
- robot_name: robot_name,
77
- input: input,
78
- output: output
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
79
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 }
80
191
  end
81
192
  end
82
193
  ```
83
194
 
84
- ## Robot Definitions
85
-
86
195
  ```ruby
87
196
  # app/robots/support_robot.rb
88
197
  class SupportRobot
89
- def self.build
90
- RobotLab.build do
91
- name "support"
92
- description "Customer support assistant"
93
-
94
- template <<~PROMPT
198
+ def self.build(user_id:)
199
+ RobotLab.build(
200
+ name: "support",
201
+ system_prompt: <<~PROMPT,
95
202
  You are a helpful customer support assistant for our company.
96
203
  Be friendly, professional, and thorough in your responses.
97
204
  If you need to look up information, use the available tools.
205
+ The current user ID is #{user_id}.
98
206
  PROMPT
99
-
100
- tool :get_user_info do
101
- description "Get information about the current user"
102
-
103
- handler do |state:, **_|
104
- user_id = state.data[:user_id]
105
- user = User.find(user_id)
106
-
107
- {
108
- name: user.name,
109
- email: user.email,
110
- plan: user.subscription&.plan || "free",
111
- member_since: user.created_at.to_date.to_s
112
- }
113
- rescue ActiveRecord::RecordNotFound
114
- { error: "User not found" }
115
- end
116
- end
117
-
118
- tool :get_orders do
119
- description "Get user's recent orders"
120
- parameter :limit, type: :integer, default: 5
121
-
122
- handler do |limit:, state:, **_|
123
- user_id = state.data[:user_id]
124
- orders = Order.where(user_id: user_id)
125
- .order(created_at: :desc)
126
- .limit(limit)
127
-
128
- orders.map do |order|
129
- {
130
- id: order.external_id,
131
- status: order.status,
132
- total: order.total.to_f,
133
- created_at: order.created_at.iso8601
134
- }
135
- end
136
- end
137
- end
138
-
139
- tool :create_ticket do
140
- description "Create a support ticket"
141
- parameter :subject, type: :string, required: true
142
- parameter :description, type: :string, required: true
143
- parameter :priority, type: :string, enum: %w[low medium high], default: "medium"
144
-
145
- handler do |subject:, description:, priority:, state:, **_|
146
- ticket = SupportTicket.create!(
147
- user_id: state.data[:user_id],
148
- subject: subject,
149
- description: description,
150
- priority: priority
151
- )
152
-
153
- {
154
- success: true,
155
- ticket_id: ticket.external_id,
156
- message: "Ticket created successfully"
157
- }
158
- rescue => e
159
- { success: false, error: e.message }
160
- end
161
- end
162
- end
207
+ local_tools: [GetUserInfoTool, GetOrdersTool, CreateTicketTool]
208
+ )
163
209
  end
164
210
  end
165
211
  ```
166
212
 
167
213
  ## Network Configuration
168
214
 
215
+ Networks use `create_network` with a block DSL that defines tasks and their dependencies:
216
+
169
217
  ```ruby
170
218
  # app/robots/support_network.rb
171
219
  class SupportNetwork
172
- def self.build
173
- RobotLab.create_network do
174
- name "support_network"
175
- default_model "claude-sonnet-4"
220
+ def self.build(user_id:)
221
+ support = SupportRobot.build(user_id: user_id)
176
222
 
177
- history RobotLab::History::ActiveRecordAdapter.new(
178
- thread_model: ConversationThread,
179
- result_model: ConversationMessage
180
- ).to_config
181
-
182
- add_robot SupportRobot.build
223
+ RobotLab.create_network(name: "support_network") do
224
+ task :support, support, depends_on: :none
183
225
  end
184
226
  end
185
227
  end
@@ -193,44 +235,17 @@ class ChatService
193
235
  def initialize(user:, thread_id: nil)
194
236
  @user = user
195
237
  @thread_id = thread_id
196
- @network = SupportNetwork.build
197
238
  end
198
239
 
199
- def call(message:, &streaming_callback)
200
- user_message = build_message(message)
201
- state = build_state(user_message)
202
-
203
- result = @network.run(state: state, user_id: @user.id) do |event|
204
- streaming_callback&.call(event)
205
- end
240
+ def call(message:)
241
+ robot = SupportRobot.build(user_id: @user.id)
242
+ result = robot.run(message)
206
243
 
207
244
  {
208
- thread_id: result.state.thread_id,
209
- response: extract_response(result),
210
- messages: result.new_results
245
+ response: result.last_text_content,
246
+ has_tool_calls: result.has_tool_calls?
211
247
  }
212
248
  end
213
-
214
- private
215
-
216
- def build_message(content)
217
- if @thread_id
218
- RobotLab::UserMessage.new(content, thread_id: @thread_id)
219
- else
220
- content
221
- end
222
- end
223
-
224
- def build_state(message)
225
- RobotLab.create_state(
226
- message: message,
227
- data: { user_id: @user.id }
228
- )
229
- end
230
-
231
- def extract_response(result)
232
- result.last_result&.output&.find { |m| m.is_a?(RobotLab::TextMessage) }&.content
233
- end
234
249
  end
235
250
  ```
236
251
 
@@ -251,7 +266,6 @@ module Api
251
266
  result = service.call(message: params[:message])
252
267
 
253
268
  render json: {
254
- thread_id: result[:thread_id],
255
269
  response: result[:response]
256
270
  }
257
271
  end
@@ -283,25 +297,15 @@ class ChatJob < ApplicationJob
283
297
 
284
298
  def perform(user_id:, thread_id:, message:)
285
299
  user = User.find(user_id)
300
+ robot = SupportRobot.build(user_id: user.id)
286
301
 
287
- service = ChatService.new(user: user, thread_id: thread_id)
288
-
289
- service.call(message: message) do |event|
290
- case event.type
291
- when :text_delta
292
- broadcast_to_user(user, type: "text", content: event.text)
293
- when :tool_call
294
- broadcast_to_user(user, type: "tool", name: event.name)
295
- when :complete
296
- broadcast_to_user(user, type: "complete")
297
- end
298
- end
299
- end
300
-
301
- private
302
+ result = robot.run(message)
302
303
 
303
- def broadcast_to_user(user, data)
304
- ChatChannel.broadcast_to(user, data)
304
+ ChatChannel.broadcast_to(
305
+ user,
306
+ type: "complete",
307
+ content: result.last_text_content
308
+ )
305
309
  end
306
310
  end
307
311
  ```
@@ -347,14 +351,8 @@ export default class extends Controller {
347
351
 
348
352
  handleMessage(data) {
349
353
  switch (data.type) {
350
- case "text":
351
- this.currentResponse.textContent += data.content
352
- break
353
- case "tool":
354
- // Show tool indicator
355
- break
356
354
  case "complete":
357
- this.threadId = data.thread_id
355
+ this.currentResponse.textContent = data.content
358
356
  break
359
357
  }
360
358
  }
@@ -397,7 +395,7 @@ yarn install
397
395
  # Setup database
398
396
  rails db:migrate
399
397
 
400
- # Set API key
398
+ # Set API key (or configure via config/robot_lab.yml)
401
399
  export ANTHROPIC_API_KEY="your-key"
402
400
 
403
401
  # Start server
@@ -406,15 +404,16 @@ bin/dev
406
404
 
407
405
  ## Key Concepts
408
406
 
409
- 1. **Robot Classes**: Encapsulate robot definitions
410
- 2. **Network Classes**: Configure multi-robot networks
411
- 3. **Service Objects**: Handle business logic
412
- 4. **Action Cable**: Real-time streaming to browser
413
- 5. **Background Jobs**: Non-blocking processing
414
- 6. **History Persistence**: ActiveRecord integration
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
415
 
416
416
  ## See Also
417
417
 
418
418
  - [Rails Integration Guide](../guides/rails-integration.md)
419
419
  - [Streaming Guide](../guides/streaming.md)
420
- - [History Guide](../guides/history.md)