robot_lab 0.0.4 → 0.0.7

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -0
  3. data/README.md +64 -6
  4. data/Rakefile +2 -1
  5. data/docs/api/core/index.md +41 -46
  6. data/docs/api/core/memory.md +200 -154
  7. data/docs/api/core/network.md +13 -3
  8. data/docs/api/core/robot.md +38 -26
  9. data/docs/api/core/state.md +55 -73
  10. data/docs/api/index.md +7 -28
  11. data/docs/api/messages/index.md +35 -20
  12. data/docs/api/messages/text-message.md +67 -21
  13. data/docs/api/messages/tool-call-message.md +80 -41
  14. data/docs/api/messages/tool-result-message.md +119 -50
  15. data/docs/api/messages/user-message.md +48 -24
  16. data/docs/architecture/core-concepts.md +10 -15
  17. data/docs/concepts.md +5 -7
  18. data/docs/examples/index.md +2 -2
  19. data/docs/getting-started/configuration.md +80 -0
  20. data/docs/guides/building-robots.md +10 -9
  21. data/docs/guides/creating-networks.md +49 -0
  22. data/docs/guides/index.md +0 -5
  23. data/docs/guides/rails-integration.md +244 -162
  24. data/docs/guides/streaming.md +118 -138
  25. data/docs/index.md +0 -8
  26. data/examples/03_network.rb +10 -7
  27. data/examples/08_llm_config.rb +40 -11
  28. data/examples/09_chaining.rb +45 -6
  29. data/examples/11_network_introspection.rb +30 -7
  30. data/examples/12_message_bus.rb +1 -1
  31. data/examples/14_rusty_circuit/heckler.rb +14 -8
  32. data/examples/14_rusty_circuit/open_mic.rb +5 -3
  33. data/examples/14_rusty_circuit/scout.rb +14 -31
  34. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +1 -1
  35. data/examples/16_writers_room/display.rb +158 -0
  36. data/examples/16_writers_room/output/.gitignore +4 -0
  37. data/examples/16_writers_room/output/README.md +69 -0
  38. data/examples/16_writers_room/output/opus_001.md +263 -0
  39. data/examples/16_writers_room/output/opus_001_notes.log +470 -0
  40. data/examples/16_writers_room/output/opus_002.md +245 -0
  41. data/examples/16_writers_room/output/opus_002_notes.log +546 -0
  42. data/examples/16_writers_room/output/opus_002_screenplay.md +7989 -0
  43. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +993 -0
  44. data/examples/16_writers_room/prompts/screenplay_writer.md +66 -0
  45. data/examples/16_writers_room/prompts/writer.md +37 -0
  46. data/examples/16_writers_room/room.rb +186 -0
  47. data/examples/16_writers_room/tools.rb +173 -0
  48. data/examples/16_writers_room/writer.rb +121 -0
  49. data/examples/16_writers_room/writers_room.rb +256 -0
  50. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
  51. data/lib/robot_lab/memory.rb +8 -32
  52. data/lib/robot_lab/network.rb +13 -20
  53. data/lib/robot_lab/robot/bus_messaging.rb +239 -0
  54. data/lib/robot_lab/robot/mcp_management.rb +88 -0
  55. data/lib/robot_lab/robot/template_rendering.rb +130 -0
  56. data/lib/robot_lab/robot.rb +56 -420
  57. data/lib/robot_lab/run_config.rb +184 -0
  58. data/lib/robot_lab/state_proxy.rb +2 -12
  59. data/lib/robot_lab/task.rb +8 -1
  60. data/lib/robot_lab/utils.rb +39 -0
  61. data/lib/robot_lab/version.rb +1 -1
  62. data/lib/robot_lab.rb +29 -8
  63. data/mkdocs.yml +0 -11
  64. metadata +21 -20
  65. data/docs/api/adapters/anthropic.md +0 -121
  66. data/docs/api/adapters/gemini.md +0 -133
  67. data/docs/api/adapters/index.md +0 -104
  68. data/docs/api/adapters/openai.md +0 -134
  69. data/docs/api/history/active-record-adapter.md +0 -275
  70. data/docs/api/history/config.md +0 -284
  71. data/docs/api/history/index.md +0 -128
  72. data/docs/api/history/thread-manager.md +0 -194
  73. data/docs/guides/history.md +0 -359
  74. data/lib/robot_lab/adapters/anthropic.rb +0 -163
  75. data/lib/robot_lab/adapters/base.rb +0 -85
  76. data/lib/robot_lab/adapters/gemini.rb +0 -193
  77. data/lib/robot_lab/adapters/openai.rb +0 -160
  78. data/lib/robot_lab/adapters/registry.rb +0 -81
  79. data/lib/robot_lab/errors.rb +0 -70
  80. data/lib/robot_lab/history/active_record_adapter.rb +0 -146
  81. data/lib/robot_lab/history/config.rb +0 -115
  82. data/lib/robot_lab/history/thread_manager.rb +0 -93
  83. data/lib/robot_lab/robotic_model.rb +0 -324
@@ -13,7 +13,7 @@ rails generate robot_lab:install
13
13
  This creates:
14
14
 
15
15
  ```
16
- config/initializers/robot_lab.rb # Configuration
16
+ config/initializers/robot_lab.rb # Logger setup
17
17
  db/migrate/*_create_robot_lab_tables.rb # Database tables
18
18
  app/models/robot_lab_thread.rb # Thread model
19
19
  app/models/robot_lab_result.rb # Result model
@@ -29,52 +29,75 @@ rails db:migrate
29
29
 
30
30
  ## Configuration
31
31
 
32
- ### Initializer
33
-
34
- ```ruby title="config/initializers/robot_lab.rb"
35
- RobotLab.configure do |config|
36
- # API Keys from credentials
37
- config.anthropic_api_key = Rails.application.credentials.anthropic_api_key
38
- config.openai_api_key = Rails.application.credentials.openai_api_key
32
+ RobotLab uses [MywayConfig](https://github.com/madbomber/myway_config) for configuration. There is no `RobotLab.configure` block. Instead, settings are loaded from YAML files and environment variables in the following priority order:
33
+
34
+ 1. **Bundled defaults** (`lib/robot_lab/config/defaults.yml`)
35
+ 2. **Environment-specific overrides** (development, test, production sections)
36
+ 3. **XDG user config** (`~/.config/robot_lab/config.yml`)
37
+ 4. **Project config** (`./config/robot_lab.yml`)
38
+ 5. **Environment variables** (`ROBOT_LAB_*` prefix)
39
+
40
+ ### Project Config File
41
+
42
+ ```yaml title="config/robot_lab.yml"
43
+ defaults:
44
+ ruby_llm:
45
+ anthropic_api_key: <%= ENV['ANTHROPIC_API_KEY'] %>
46
+ openai_api_key: <%= ENV['OPENAI_API_KEY'] %>
47
+ model: claude-sonnet-4
48
+ request_timeout: 180
49
+
50
+ # Template path auto-detected as app/prompts in Rails
51
+ # template_path: app/prompts
52
+
53
+ development:
54
+ ruby_llm:
55
+ model: claude-haiku-3
56
+ log_level: :debug
57
+
58
+ test:
59
+ streaming_enabled: false
60
+ ruby_llm:
61
+ model: claude-3-haiku-20240307
62
+ request_timeout: 30
63
+
64
+ production:
65
+ ruby_llm:
66
+ request_timeout: 180
67
+ max_retries: 5
68
+ ```
39
69
 
40
- # Defaults
41
- config.default_provider = :anthropic
42
- config.default_model = "claude-sonnet-4"
70
+ ### Environment Variables
43
71
 
44
- # Rails logger
45
- config.logger = Rails.logger
72
+ Environment variables use the `ROBOT_LAB_` prefix with double underscores for nested keys:
46
73
 
47
- # Template path (auto-configured to app/prompts)
48
- end
74
+ ```bash
75
+ ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
76
+ ROBOT_LAB_RUBY_LLM__MODEL=claude-sonnet-4
77
+ ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180
49
78
  ```
50
79
 
51
- ### Environment-Specific
80
+ RobotLab also falls back to standard provider environment variables (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`) when the prefixed versions are not set.
52
81
 
53
- ```ruby
54
- RobotLab.configure do |config|
55
- config.anthropic_api_key = Rails.application.credentials.anthropic_api_key
56
-
57
- case Rails.env
58
- when "development"
59
- config.default_model = "claude-haiku-3" # Faster/cheaper
60
- config.logger.level = :debug
61
- when "test"
62
- config.streaming_enabled = false
63
- when "production"
64
- config.default_model = "claude-sonnet-4"
65
- end
66
- end
82
+ ### Initializer (Logger Only)
83
+
84
+ The only runtime-writable config attribute is the logger. The generated initializer sets it to the Rails logger:
85
+
86
+ ```ruby title="config/initializers/robot_lab.rb"
87
+ # frozen_string_literal: true
88
+
89
+ # Set the RobotLab logger to use Rails.logger
90
+ RobotLab.config.logger = Rails.logger
67
91
  ```
68
92
 
69
- ### Application Config
93
+ ### Accessing Configuration
70
94
 
71
- ```ruby title="config/application.rb"
72
- module MyApp
73
- class Application < Rails::Application
74
- config.robot_lab.default_model = "claude-sonnet-4"
75
- config.robot_lab.default_provider = :anthropic
76
- end
77
- end
95
+ ```ruby
96
+ # Read configuration values
97
+ RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
98
+ RobotLab.config.ruby_llm.anthropic_api_key #=> "sk-ant-..."
99
+ RobotLab.config.ruby_llm.request_timeout #=> 120
100
+ RobotLab.config.streaming_enabled #=> true
78
101
  ```
79
102
 
80
103
  ## Creating Robots
@@ -89,22 +112,45 @@ rails generate robot_lab:robot Router --routing
89
112
 
90
113
  ### Robot Class
91
114
 
115
+ Robots are plain Ruby classes with a `.build` factory method that calls `RobotLab.build` with keyword arguments:
116
+
92
117
  ```ruby title="app/robots/support_robot.rb"
118
+ # frozen_string_literal: true
119
+
93
120
  class SupportRobot
94
121
  def self.build
95
- RobotLab.build do
96
- name "support"
97
- description "Handles customer support inquiries"
98
- model "claude-sonnet-4"
99
-
100
- template "support/system_prompt"
101
-
102
- tool :lookup_order do
103
- description "Look up order by ID"
104
- parameter :order_id, type: :string, required: true
105
- handler { |order_id:, **_| Order.find_by(id: order_id)&.to_h }
106
- end
107
- end
122
+ RobotLab.build(
123
+ name: "support",
124
+ description: "Handles customer support inquiries",
125
+ model: "claude-sonnet-4",
126
+ template: :support,
127
+ local_tools: [OrderLookup.new]
128
+ )
129
+ end
130
+ end
131
+ ```
132
+
133
+ ### Custom Tool
134
+
135
+ Tools subclass `RobotLab::Tool` (which extends `RubyLLM::Tool`):
136
+
137
+ ```ruby title="app/tools/order_lookup.rb"
138
+ # frozen_string_literal: true
139
+
140
+ class OrderLookup < RobotLab::Tool
141
+ description "Look up an order by ID"
142
+ param :order_id, type: "string", desc: "The order ID to look up"
143
+
144
+ def execute(order_id:)
145
+ order = Order.find_by(id: order_id)
146
+ return "Order not found" unless order
147
+
148
+ {
149
+ id: order.id,
150
+ status: order.status,
151
+ total: order.total.to_s,
152
+ created_at: order.created_at.iso8601
153
+ }.to_json
108
154
  end
109
155
  end
110
156
  ```
@@ -114,37 +160,40 @@ end
114
160
  ```ruby title="app/controllers/chat_controller.rb"
115
161
  class ChatController < ApplicationController
116
162
  def create
117
- network = build_network
118
- state = RobotLab.create_state(
119
- message: params[:message],
120
- data: { user_id: current_user.id }
121
- )
122
-
123
- result = network.run(state: state)
163
+ robot = SupportRobot.build
164
+ result = robot.run(params[:message])
124
165
 
125
166
  render json: {
126
- response: result.last_result.output.first.content,
127
- thread_id: state.thread_id
167
+ response: result.last_text_content,
168
+ robot_name: result.robot_name
128
169
  }
129
170
  end
171
+ end
172
+ ```
130
173
 
131
- private
174
+ ### Using a Network in Controllers
132
175
 
133
- def build_network
134
- RobotLab.create_network do
135
- name "customer_service"
136
- add_robot SupportRobot.build
137
- add_robot BillingRobot.build
176
+ Networks use `RobotLab.create_network` with a block DSL that defines tasks. Each task wraps a robot with dependency declarations:
138
177
 
139
- history history_adapter.to_config
178
+ ```ruby title="app/controllers/chat_controller.rb"
179
+ class ChatController < ApplicationController
180
+ def create
181
+ support_robot = SupportRobot.build
182
+ billing_robot = BillingRobot.build
183
+
184
+ network = RobotLab.create_network(name: "customer_service") do
185
+ task :support, support_robot, depends_on: :none
186
+ task :billing, billing_robot, depends_on: :optional
140
187
  end
141
- end
142
188
 
143
- def history_adapter
144
- RobotLab::History::ActiveRecordAdapter.new(
145
- thread_model: RobotLabThread,
146
- result_model: RobotLabResult
147
- )
189
+ result = network.run(message: params[:message], user_id: current_user.id)
190
+
191
+ # result is a SimpleFlow::Result
192
+ # result.value is a RobotResult from the last robot
193
+ render json: {
194
+ response: result.value.last_text_content,
195
+ robot_name: result.value.robot_name
196
+ }
148
197
  end
149
198
  end
150
199
  ```
@@ -153,26 +202,28 @@ end
153
202
 
154
203
  ### Template Location
155
204
 
205
+ Templates are `.md` files with YAML front matter, stored in `app/prompts/` (auto-configured for Rails):
206
+
156
207
  ```
157
208
  app/prompts/
158
- ├── support/
159
- ├── system_prompt.erb
160
- └── greeting.erb
161
- └── billing/
162
- └── system_prompt.erb
209
+ ├── support.md
210
+ ├── billing.md
211
+ └── router.md
163
212
  ```
164
213
 
165
- ### Template Usage
166
-
167
- ```ruby
168
- robot = RobotLab.build do
169
- name "support"
170
- template "support/system_prompt", company: "Acme Corp"
171
- end
172
- ```
214
+ ### Template Format
173
215
 
174
- ```erb title="app/prompts/support/system_prompt.erb"
175
- You are a support agent for <%= company %>.
216
+ ```markdown title="app/prompts/support.md"
217
+ ---
218
+ description: Customer support assistant
219
+ parameters:
220
+ company_name: null
221
+ tone: friendly
222
+ model: claude-sonnet-4
223
+ temperature: 0.7
224
+ ---
225
+ You are a support agent for <%= company_name %>.
226
+ Respond in a <%= tone %> manner.
176
227
 
177
228
  Your responsibilities:
178
229
  - Answer product questions
@@ -180,6 +231,21 @@ Your responsibilities:
180
231
  - Provide friendly assistance
181
232
  ```
182
233
 
234
+ ### Template Usage
235
+
236
+ ```ruby
237
+ # Pass context to fill template parameters
238
+ robot = RobotLab.build(
239
+ name: "support",
240
+ template: :support,
241
+ context: { company_name: "Acme Corp" }
242
+ )
243
+
244
+ # Parameters with defaults (like `tone: friendly`) are optional.
245
+ # Parameters set to null are required and must be provided via context.
246
+ result = robot.run("I need help with my order")
247
+ ```
248
+
183
249
  ## Action Cable Integration
184
250
 
185
251
  ### Channel
@@ -187,29 +253,25 @@ Your responsibilities:
187
253
  ```ruby title="app/channels/chat_channel.rb"
188
254
  class ChatChannel < ApplicationCable::Channel
189
255
  def subscribed
190
- stream_from "chat_#{params[:thread_id]}"
256
+ stream_from "chat_#{params[:session_id]}"
191
257
  end
192
258
 
193
259
  def receive(data)
194
- message = data["message"]
195
- thread_id = data["thread_id"]
260
+ message = data["message"]
261
+ session_id = data["session_id"]
196
262
 
197
- state = RobotLab.create_state(message: message)
198
- state.thread_id = thread_id if thread_id
263
+ robot = SupportRobot.build
264
+ result = robot.run(message)
199
265
 
200
- network.run(
201
- state: state,
202
- streaming: ->(event) {
203
- ActionCable.server.broadcast("chat_#{thread_id || state.thread_id}", event)
266
+ ActionCable.server.broadcast(
267
+ "chat_#{session_id}",
268
+ {
269
+ event: "complete",
270
+ response: result.last_text_content,
271
+ robot_name: result.robot_name
204
272
  }
205
273
  )
206
274
  end
207
-
208
- private
209
-
210
- def network
211
- @network ||= ChatNetwork.build
212
- end
213
275
  end
214
276
  ```
215
277
 
@@ -217,17 +279,17 @@ end
217
279
 
218
280
  ```javascript
219
281
  const channel = consumer.subscriptions.create(
220
- { channel: "ChatChannel", thread_id: threadId },
282
+ { channel: "ChatChannel", session_id: sessionId },
221
283
  {
222
284
  received(data) {
223
- if (data.event === "delta") {
224
- appendToMessage(data.data.content);
285
+ if (data.event === "complete") {
286
+ displayMessage(data.response);
225
287
  }
226
288
  }
227
289
  }
228
290
  );
229
291
 
230
- channel.send({ message: "Hello!", thread_id: threadId });
292
+ channel.send({ message: "Hello!", session_id: sessionId });
231
293
  ```
232
294
 
233
295
  ## Background Jobs
@@ -238,27 +300,20 @@ channel.send({ message: "Hello!", thread_id: threadId });
238
300
  class ProcessMessageJob < ApplicationJob
239
301
  queue_as :default
240
302
 
241
- def perform(thread_id:, message:, user_id:)
242
- state = RobotLab.create_state(
243
- message: message,
244
- data: { user_id: user_id }
245
- )
246
- state.thread_id = thread_id
247
-
248
- result = network.run(state: state)
303
+ def perform(session_id:, message:, user_id:)
304
+ robot = SupportRobot.build
305
+ result = robot.run(message)
249
306
 
250
- # Notify user of completion
307
+ # Notify user of completion via Action Cable
251
308
  ActionCable.server.broadcast(
252
- "chat_#{thread_id}",
253
- { event: "complete", response: result.last_result.output.first.content }
309
+ "chat_#{session_id}",
310
+ {
311
+ event: "complete",
312
+ response: result.last_text_content,
313
+ robot_name: result.robot_name
314
+ }
254
315
  )
255
316
  end
256
-
257
- private
258
-
259
- def network
260
- ChatNetwork.build
261
- end
262
317
  end
263
318
  ```
264
319
 
@@ -266,7 +321,7 @@ end
266
321
 
267
322
  ```ruby
268
323
  ProcessMessageJob.perform_later(
269
- thread_id: params[:thread_id],
324
+ session_id: params[:session_id],
270
325
  message: params[:message],
271
326
  user_id: current_user.id
272
327
  )
@@ -278,10 +333,16 @@ render json: { status: "processing" }
278
333
 
279
334
  ### Test Configuration
280
335
 
281
- ```ruby title="config/environments/test.rb"
282
- Rails.application.configure do
283
- config.robot_lab.streaming_enabled = false
284
- end
336
+ Use `config/robot_lab.yml` to configure the test environment with a faster, cheaper model:
337
+
338
+ ```yaml title="config/robot_lab.yml"
339
+ test:
340
+ max_iterations: 3
341
+ streaming_enabled: false
342
+ ruby_llm:
343
+ model: claude-3-haiku-20240307
344
+ request_timeout: 30
345
+ max_retries: 1
285
346
  ```
286
347
 
287
348
  ### Robot Tests
@@ -293,7 +354,17 @@ class SupportRobotTest < ActiveSupport::TestCase
293
354
  test "builds valid robot" do
294
355
  robot = SupportRobot.build
295
356
  assert_equal "support", robot.name
296
- assert_includes robot.tools.map(&:name), "lookup_order"
357
+ end
358
+
359
+ test "robot has correct model" do
360
+ robot = SupportRobot.build
361
+ assert_equal "claude-sonnet-4", robot.model
362
+ end
363
+
364
+ test "robot has local tools" do
365
+ robot = SupportRobot.build
366
+ tool_names = robot.local_tools.map(&:name)
367
+ assert_includes tool_names, "order_lookup"
297
368
  end
298
369
  end
299
370
  ```
@@ -311,7 +382,6 @@ class ChatTest < ActionDispatch::IntegrationTest
311
382
 
312
383
  json = JSON.parse(response.body)
313
384
  assert json["response"].present?
314
- assert json["thread_id"].present?
315
385
  end
316
386
  end
317
387
  end
@@ -323,11 +393,21 @@ end
323
393
 
324
394
  ```ruby title="app/models/robot_lab_thread.rb"
325
395
  class RobotLabThread < ApplicationRecord
326
- has_many :results, class_name: "RobotLabResult", foreign_key: :thread_id
327
- belongs_to :user, optional: true
396
+ has_many :results,
397
+ class_name: "RobotLabResult",
398
+ foreign_key: :session_id,
399
+ primary_key: :session_id,
400
+ dependent: :destroy
328
401
 
329
- scope :recent, -> { order(updated_at: :desc) }
330
- scope :for_user, ->(user) { where(user: user) }
402
+ validates :session_id, presence: true, uniqueness: true
403
+
404
+ def self.find_or_create_by_session_id(id)
405
+ find_or_create_by(session_id: id)
406
+ end
407
+
408
+ def last_result
409
+ results.order(sequence_number: :desc).first
410
+ end
331
411
  end
332
412
  ```
333
413
 
@@ -335,23 +415,24 @@ end
335
415
 
336
416
  ```ruby title="app/models/robot_lab_result.rb"
337
417
  class RobotLabResult < ApplicationRecord
338
- belongs_to :thread, class_name: "RobotLabThread"
418
+ belongs_to :thread,
419
+ class_name: "RobotLabThread",
420
+ foreign_key: :session_id,
421
+ primary_key: :session_id
422
+
423
+ validates :session_id, presence: true
424
+ validates :robot_name, presence: true
425
+
426
+ default_scope { order(sequence_number: :asc) }
339
427
 
340
428
  def to_robot_result
341
429
  RobotLab::RobotResult.new(
342
430
  robot_name: robot_name,
343
- output: deserialize_messages(output_data),
344
- tool_calls: deserialize_messages(tool_calls_data),
431
+ output: (output_data || []).map { |d| RobotLab::Message.from_hash(d.symbolize_keys) },
432
+ tool_calls: (tool_calls_data || []).map { |d| RobotLab::Message.from_hash(d.symbolize_keys) },
345
433
  stop_reason: stop_reason
346
434
  )
347
435
  end
348
-
349
- private
350
-
351
- def deserialize_messages(data)
352
- return [] unless data
353
- data.map { |h| RobotLab::Message.from_hash(h.symbolize_keys) }
354
- end
355
436
  end
356
437
  ```
357
438
 
@@ -365,29 +446,31 @@ class ChatService
365
446
  @user = user
366
447
  end
367
448
 
368
- def process(message, thread_id: nil)
369
- state = build_state(message, thread_id)
370
- result = network.run(state: state)
449
+ def process(message)
450
+ robot = SupportRobot.build
451
+ result = robot.run(message)
371
452
 
372
453
  {
373
- response: result.last_result.output.first.content,
374
- thread_id: state.thread_id
454
+ response: result.last_text_content,
455
+ robot_name: result.robot_name
375
456
  }
376
457
  end
377
458
 
378
- private
459
+ def process_with_network(message)
460
+ support_robot = SupportRobot.build
461
+ billing_robot = BillingRobot.build
379
462
 
380
- def build_state(message, thread_id)
381
- state = RobotLab.create_state(
382
- message: message,
383
- data: { user_id: @user.id }
384
- )
385
- state.thread_id = thread_id if thread_id
386
- state
387
- end
463
+ network = RobotLab.create_network(name: "customer_service") do
464
+ task :support, support_robot, depends_on: :none
465
+ task :billing, billing_robot, depends_on: :optional
466
+ end
467
+
468
+ result = network.run(message: message, user_id: @user.id)
388
469
 
389
- def network
390
- @network ||= ChatNetwork.build
470
+ {
471
+ response: result.value.last_text_content,
472
+ robot_name: result.value.robot_name
473
+ }
391
474
  end
392
475
  end
393
476
  ```
@@ -429,4 +512,3 @@ end
429
512
 
430
513
  - [Building Robots](building-robots.md) - Robot patterns
431
514
  - [Creating Networks](creating-networks.md) - Network configuration
432
- - [History Guide](history.md) - Conversation persistence