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
@@ -7,16 +7,18 @@ User input with conversation metadata.
7
7
  ```ruby
8
8
  message = UserMessage.new(
9
9
  "What's my order status?",
10
- thread_id: "thread_123",
10
+ session_id: "session_123",
11
11
  system_prompt: "Be concise",
12
12
  metadata: { source: "web" }
13
13
  )
14
14
  ```
15
15
 
16
+ **Note:** `UserMessage` is a standalone class, not a subclass of `Message`.
17
+
16
18
  ## Constructor
17
19
 
18
20
  ```ruby
19
- UserMessage.new(content, thread_id: nil, system_prompt: nil, metadata: {})
21
+ UserMessage.new(content, session_id: nil, system_prompt: nil, metadata: nil, id: nil)
20
22
  ```
21
23
 
22
24
  **Parameters:**
@@ -24,9 +26,10 @@ UserMessage.new(content, thread_id: nil, system_prompt: nil, metadata: {})
24
26
  | Name | Type | Description |
25
27
  |------|------|-------------|
26
28
  | `content` | `String` | Message text |
27
- | `thread_id` | `String`, `nil` | Conversation thread ID |
29
+ | `session_id` | `String`, `nil` | Conversation session ID |
28
30
  | `system_prompt` | `String`, `nil` | Override system prompt |
29
- | `metadata` | `Hash` | Additional metadata |
31
+ | `metadata` | `Hash`, `nil` | Additional metadata |
32
+ | `id` | `String`, `nil` | Unique message ID (defaults to UUID) |
30
33
 
31
34
  ## Attributes
32
35
 
@@ -38,13 +41,13 @@ message.content # => String
38
41
 
39
42
  The message text.
40
43
 
41
- ### thread_id
44
+ ### session_id
42
45
 
43
46
  ```ruby
44
- message.thread_id # => String | nil
47
+ message.session_id # => String | nil
45
48
  ```
46
49
 
47
- Conversation thread identifier for history persistence.
50
+ Conversation session identifier for history persistence.
48
51
 
49
52
  ### system_prompt
50
53
 
@@ -78,14 +81,6 @@ message.created_at # => Time
78
81
 
79
82
  Message creation timestamp.
80
83
 
81
- ### role
82
-
83
- ```ruby
84
- message.role # => :user
85
- ```
86
-
87
- Always returns `:user`.
88
-
89
84
  ## Methods
90
85
 
91
86
  ### to_h
@@ -100,10 +95,11 @@ Hash representation.
100
95
 
101
96
  ```ruby
102
97
  {
103
- role: :user,
104
98
  content: "What's my order status?",
99
+ session_id: "session_123",
100
+ system_prompt: "Be concise",
101
+ metadata: { source: "web" },
105
102
  id: "uuid-here",
106
- thread_id: "thread_123",
107
103
  created_at: "2024-01-15T10:30:00Z"
108
104
  }
109
105
  ```
@@ -116,6 +112,30 @@ message.to_json # => String
116
112
 
117
113
  JSON representation.
118
114
 
115
+ ### to_message
116
+
117
+ ```ruby
118
+ message.to_message # => TextMessage
119
+ ```
120
+
121
+ Converts to a `TextMessage` with role `"user"` for use in conversation history.
122
+
123
+ ### to_s
124
+
125
+ ```ruby
126
+ message.to_s # => String
127
+ ```
128
+
129
+ Returns the content string.
130
+
131
+ ### self.from
132
+
133
+ ```ruby
134
+ UserMessage.from(input) # => UserMessage
135
+ ```
136
+
137
+ Creates a `UserMessage` from a String, Hash, or existing `UserMessage`.
138
+
119
139
  ## Examples
120
140
 
121
141
  ### Basic Message
@@ -124,12 +144,12 @@ JSON representation.
124
144
  message = UserMessage.new("Hello!")
125
145
  ```
126
146
 
127
- ### With Thread ID
147
+ ### With Session ID
128
148
 
129
149
  ```ruby
130
150
  message = UserMessage.new(
131
151
  "Continue our conversation",
132
- thread_id: "thread_abc123"
152
+ session_id: "session_abc123"
133
153
  )
134
154
  ```
135
155
 
@@ -156,16 +176,20 @@ message = UserMessage.new(
156
176
  )
157
177
  ```
158
178
 
159
- ### Creating State
179
+ ### Creating from Various Inputs
160
180
 
161
181
  ```ruby
162
- message = UserMessage.new("Help", thread_id: "thread_123")
163
- state = RobotLab.create_state(message: message)
182
+ # From a string
183
+ msg = UserMessage.from("Hello!")
184
+
185
+ # From a hash
186
+ msg = UserMessage.from(content: "Hello!", session_id: "123")
164
187
 
165
- state.thread_id # => "thread_123"
188
+ # From an existing UserMessage (returns as-is)
189
+ msg = UserMessage.from(existing_message)
166
190
  ```
167
191
 
168
192
  ## See Also
169
193
 
170
- - [State](../core/state.md)
194
+ - [Memory](../core/memory.md)
171
195
  - [TextMessage](text-message.md)
@@ -130,22 +130,21 @@ robot = RobotLab.build(
130
130
  )
131
131
  ```
132
132
 
133
- ### RobotLab::Tool Inline
133
+ ### RobotLab::Tool.create Factory
134
134
 
135
135
  For simpler tools that do not need a class:
136
136
 
137
137
  ```ruby
138
- tool = RobotLab::Tool.new(
138
+ tool = RobotLab::Tool.create(
139
139
  name: "get_time",
140
- description: "Get the current time",
141
- handler: ->(_input, **_opts) { Time.now.to_s }
142
- )
140
+ description: "Get the current time"
141
+ ) { |_args| Time.now.to_s }
143
142
  ```
144
143
 
145
144
  With parameter schema:
146
145
 
147
146
  ```ruby
148
- tool = RobotLab::Tool.new(
147
+ tool = RobotLab::Tool.create(
149
148
  name: "get_weather",
150
149
  description: "Get weather for a location",
151
150
  parameters: {
@@ -154,11 +153,8 @@ tool = RobotLab::Tool.new(
154
153
  location: { type: "string", description: "City name" }
155
154
  },
156
155
  required: ["location"]
157
- },
158
- handler: ->(input, **_opts) {
159
- WeatherAPI.current(input[:location])
160
156
  }
161
- )
157
+ ) { |args| WeatherAPI.current(args[:location]) }
162
158
  ```
163
159
 
164
160
  ### Tool Execution
@@ -167,10 +163,9 @@ When an LLM decides to use a tool:
167
163
 
168
164
  1. LLM generates a tool call with tool name and arguments
169
165
  2. `@chat` (RubyLLM) identifies the tool from its registered tools
170
- 3. For `RubyLLM::Tool`: calls the `execute` method with keyword arguments
171
- 4. For `RobotLab::Tool`: calls the `call` method with input hash and context
172
- 5. Result is sent back to the LLM for continued processing
173
- 6. Loop repeats until the LLM produces a final text response
166
+ 3. Calls the `execute` method with keyword arguments
167
+ 4. Result is sent back to the LLM for continued processing
168
+ 5. Loop repeats until the LLM produces a final text response
174
169
 
175
170
  ### Error Handling
176
171
 
@@ -414,7 +409,7 @@ alice.send_message(to: :bob, content: "Tell me a joke.")
414
409
  # Handle incoming messages with auto-ack (1 arg)
415
410
  bob.on_message do |message|
416
411
  joke = bob.run(message.content.to_s).last_text_content
417
- bob.reply(message, joke)
412
+ bob.send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
418
413
  end
419
414
  ```
420
415
 
data/docs/concepts.md CHANGED
@@ -167,10 +167,10 @@ robot = RobotLab.build(
167
167
  )
168
168
  ```
169
169
 
170
- ### RobotLab::Tool with Block Handler
170
+ ### RobotLab::Tool.create Factory
171
171
 
172
172
  ```ruby
173
- tool = RobotLab::Tool.new(
173
+ tool = RobotLab::Tool.create(
174
174
  name: "get_weather",
175
175
  description: "Get current weather for a location",
176
176
  parameters: {
@@ -180,9 +180,7 @@ tool = RobotLab::Tool.new(
180
180
  },
181
181
  required: ["location"]
182
182
  }
183
- ) do |input, **_opts|
184
- WeatherService.current(input[:location])
185
- end
183
+ ) { |args| WeatherService.current(args[:location]) }
186
184
  ```
187
185
 
188
186
  ## RobotResult
@@ -374,7 +372,7 @@ alice = RobotLab.build(name: "alice", system_prompt: "You evaluate jokes.", bus:
374
372
  # Register handlers
375
373
  bob.on_message do |message|
376
374
  joke = bob.run(message.content.to_s).last_text_content
377
- bob.reply(message, joke)
375
+ bob.send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
378
376
  end
379
377
 
380
378
  alice.on_message do |message|
@@ -391,7 +389,7 @@ Key features:
391
389
 
392
390
  - **Typed channels** — only `RobotMessage` objects accepted per channel
393
391
  - **Auto-ACK** — 1-arg `on_message` blocks auto-acknowledge; 2-arg blocks give manual control
394
- - **Reply correlation** — `reply(message, content)` tracks threads via `in_reply_to`
392
+ - **Reply correlation** — `send_reply(to:, content:, in_reply_to:)` tracks threads via `in_reply_to`
395
393
  - **Independent of Network** — bus works without a Network pipeline
396
394
 
397
395
  ### Dynamic Spawning
@@ -190,7 +190,7 @@ class Comedian < RobotLab::Robot
190
190
  temp = [TEMP_START + TEMP_STEP * (@attempts - 1), 1.0].min
191
191
  with_temperature(temp)
192
192
  joke = run(message.content.to_s).last_text_content.strip
193
- reply(message, joke)
193
+ send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
194
194
  end
195
195
  end
196
196
 
@@ -224,7 +224,7 @@ Key patterns demonstrated:
224
224
 
225
225
  - **Robot subclasses** with templates for prompt management
226
226
  - **Auto-ack** via 1-arg `on_message` blocks
227
- - **`reply(message, content)`** for correlated responses
227
+ - **`send_reply(to:, content:, in_reply_to:)`** for correlated responses
228
228
  - **Temperature ramping** (0.2 &rarr; 1.0) for increasing creativity
229
229
  - **Convergence loop** that terminates when the critic approves
230
230
 
@@ -292,6 +292,86 @@ defaults:
292
292
  anthropic_api_key: <%= Rails.application.credentials.anthropic_api_key %>
293
293
  ```
294
294
 
295
+ ## RunConfig: Shared Operational Defaults
296
+
297
+ `RunConfig` is a configuration object that lets you express operational defaults for LLM settings, tools, callbacks, and infrastructure. Unlike `RobotLab.config` (which is global and static), RunConfig flows through the hierarchy and can be customized at each level:
298
+
299
+ ```
300
+ RobotLab.config (global) -> Network RunConfig -> Robot RunConfig -> Template front matter -> Task RunConfig -> Runtime
301
+ ```
302
+
303
+ ### Creating a RunConfig
304
+
305
+ ```ruby
306
+ # Keyword construction
307
+ config = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.7)
308
+
309
+ # Block DSL
310
+ config = RobotLab::RunConfig.new do |c|
311
+ c.model "claude-sonnet-4"
312
+ c.temperature 0.7
313
+ c.max_tokens 2000
314
+ end
315
+
316
+ # Chaining
317
+ config = RobotLab::RunConfig.new
318
+ .model("claude-sonnet-4")
319
+ .temperature(0.7)
320
+ ```
321
+
322
+ ### Applying RunConfig
323
+
324
+ Pass `config:` to robots and networks. Explicit constructor kwargs always override the RunConfig:
325
+
326
+ ```ruby
327
+ # Shared config for a team of robots
328
+ shared = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
329
+
330
+ # Robot uses shared config
331
+ robot = RobotLab.build(
332
+ name: "writer",
333
+ system_prompt: "You are a creative writer.",
334
+ config: shared,
335
+ temperature: 0.9 # overrides shared config's 0.5
336
+ )
337
+
338
+ # Network applies config to all member robots
339
+ network = RobotLab.create_network(name: "pipeline", config: shared) do
340
+ task :analyzer, analyzer_robot, depends_on: :none
341
+ task :writer, writer_robot, depends_on: [:analyzer]
342
+ end
343
+ ```
344
+
345
+ ### Merging Configs
346
+
347
+ RunConfig supports merge semantics where the more-specific config's values win:
348
+
349
+ ```ruby
350
+ network_config = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
351
+ robot_config = RobotLab::RunConfig.new(temperature: 0.9)
352
+ effective = network_config.merge(robot_config)
353
+ effective.model #=> "claude-sonnet-4" (inherited)
354
+ effective.temperature #=> 0.9 (overridden)
355
+ ```
356
+
357
+ ### Available Fields
358
+
359
+ | Category | Fields |
360
+ |----------|--------|
361
+ | **LLM** | `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` |
362
+ | **Tools** | `mcp`, `tools` |
363
+ | **Callbacks** | `on_tool_call`, `on_tool_result` |
364
+ | **Infrastructure** | `bus`, `enable_cache` |
365
+
366
+ ### RunConfig vs RobotLab.config
367
+
368
+ | | `RobotLab.config` | `RunConfig` |
369
+ |---|---|---|
370
+ | **Scope** | Global (all robots) | Per-network, per-robot, or per-task |
371
+ | **Source** | YAML files, env vars | Code (constructor, block DSL) |
372
+ | **Mutability** | Loaded once, rarely changed | Created per use case, merged |
373
+ | **Purpose** | API keys, timeouts, defaults | Model, temperature, tools per workflow |
374
+
295
375
  ## Robot-Level Configuration
296
376
 
297
377
  Individual robots can override the global model and other settings:
@@ -340,17 +340,18 @@ puts result.value.last_text_content
340
340
 
341
341
  ### With Streaming
342
342
 
343
- Stream responses in real-time by passing a block:
343
+ Stream responses in real-time by registering callbacks before calling `run`:
344
344
 
345
345
  ```ruby
346
- robot.run("Tell me a story") do |event|
347
- case event[:event]
348
- when "text.delta"
349
- print event[:data][:content]
350
- when "tool_call"
351
- puts "\nCalling tool: #{event[:data][:name]}"
352
- end
346
+ robot.on_new_message do |message|
347
+ print message.content if message.content
348
+ end
349
+
350
+ robot.on_tool_call do |tool_call|
351
+ puts "\nCalling tool: #{tool_call.name}"
353
352
  end
353
+
354
+ result = robot.run("Tell me a story")
354
355
  ```
355
356
 
356
357
  ## Robot Patterns
@@ -434,7 +435,7 @@ class Comedian < RobotLab::Robot
434
435
  super(name: "bob", template: :comedian, bus: bus)
435
436
  on_message do |message|
436
437
  joke = run(message.content.to_s).last_text_content.strip
437
- reply(message, joke)
438
+ send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
438
439
  end
439
440
  end
440
441
  end
@@ -122,6 +122,7 @@ end
122
122
  | `mcp` | MCP servers for this task (`:none`, `:inherit`, or array) |
123
123
  | `tools` | Tools available to this task (`:none`, `:inherit`, or array) |
124
124
  | `memory` | Task-specific memory |
125
+ | `config` | Per-task `RunConfig` (merged on top of network's config) |
125
126
  | `depends_on` | `:none`, `[:task1]`, or `:optional` |
126
127
 
127
128
  ## Conditional Routing
@@ -380,6 +381,54 @@ network.memory # => Memory instance (shared)
380
381
  network.to_h # => Hash representation
381
382
  ```
382
383
 
384
+ ## Configuration Inheritance
385
+
386
+ Networks accept a `config:` parameter that establishes default LLM settings for all member robots. This is useful when you want consistent behavior across a pipeline without configuring each robot individually.
387
+
388
+ ### Network-Wide Defaults
389
+
390
+ ```ruby
391
+ # All robots in this network use the same model and temperature
392
+ shared = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
393
+
394
+ network = RobotLab.create_network(name: "pipeline", config: shared) do
395
+ task :analyzer, analyzer_robot, depends_on: :none
396
+ task :writer, writer_robot, depends_on: [:analyzer]
397
+ task :reviewer, reviewer_robot, depends_on: [:writer]
398
+ end
399
+ ```
400
+
401
+ ### Per-Task Overrides
402
+
403
+ Individual tasks can override the network's config with their own `config:`:
404
+
405
+ ```ruby
406
+ creative_config = RobotLab::RunConfig.new(temperature: 0.9)
407
+
408
+ network = RobotLab.create_network(name: "pipeline", config: shared) do
409
+ task :analyzer, analyzer_robot, depends_on: :none
410
+ task :writer, writer_robot,
411
+ config: creative_config, # writer gets higher temperature
412
+ depends_on: [:analyzer]
413
+ task :reviewer, reviewer_robot, depends_on: [:writer]
414
+ end
415
+ ```
416
+
417
+ ### Inheritance Chain
418
+
419
+ The full configuration hierarchy (most-specific wins):
420
+
421
+ ```
422
+ RobotLab.config (global)
423
+ -> Network config
424
+ -> Task config
425
+ -> Robot config (from constructor)
426
+ -> Template front matter
427
+ -> Constructor kwargs (model:, temperature:, etc.)
428
+ ```
429
+
430
+ Each layer only overrides values it explicitly sets. Unset values pass through from the parent.
431
+
383
432
  ## Best Practices
384
433
 
385
434
  ### 1. Keep Robots Focused
data/docs/guides/index.md CHANGED
@@ -34,10 +34,6 @@ If you're new to RobotLab, start here:
34
34
 
35
35
  Real-time streaming of LLM responses
36
36
 
37
- - [:octicons-database-24: **Conversation History**](history.md)
38
-
39
- Persist and restore conversation threads
40
-
41
37
  - [:octicons-cpu-24: **Memory System**](memory.md)
42
38
 
43
39
  Share data between robots with the memory system
@@ -63,6 +59,5 @@ If you're new to RobotLab, start here:
63
59
  | [Using Tools](using-tools.md) | Add custom capabilities | 10 min |
64
60
  | [MCP Integration](mcp-integration.md) | External tool servers | 10 min |
65
61
  | [Streaming](streaming.md) | Real-time responses | 5 min |
66
- | [History](history.md) | Conversation persistence | 10 min |
67
62
  | [Memory](memory.md) | Shared data store | 5 min |
68
63
  | [Rails Integration](rails-integration.md) | Rails application setup | 15 min |