robot_lab 0.0.4 → 0.0.6

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -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 +2 -0
  37. data/examples/16_writers_room/output/opus_001.md +263 -0
  38. data/examples/16_writers_room/output/opus_001_notes.log +470 -0
  39. data/examples/16_writers_room/prompts/writer.md +37 -0
  40. data/examples/16_writers_room/room.rb +150 -0
  41. data/examples/16_writers_room/tools.rb +162 -0
  42. data/examples/16_writers_room/writer.rb +121 -0
  43. data/examples/16_writers_room/writers_room.rb +162 -0
  44. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
  45. data/lib/robot_lab/memory.rb +8 -32
  46. data/lib/robot_lab/network.rb +13 -20
  47. data/lib/robot_lab/robot/bus_messaging.rb +239 -0
  48. data/lib/robot_lab/robot/mcp_management.rb +88 -0
  49. data/lib/robot_lab/robot/template_rendering.rb +130 -0
  50. data/lib/robot_lab/robot.rb +56 -420
  51. data/lib/robot_lab/run_config.rb +184 -0
  52. data/lib/robot_lab/state_proxy.rb +2 -12
  53. data/lib/robot_lab/task.rb +8 -1
  54. data/lib/robot_lab/utils.rb +39 -0
  55. data/lib/robot_lab/version.rb +1 -1
  56. data/lib/robot_lab.rb +29 -8
  57. data/mkdocs.yml +0 -11
  58. metadata +15 -20
  59. data/docs/api/adapters/anthropic.md +0 -121
  60. data/docs/api/adapters/gemini.md +0 -133
  61. data/docs/api/adapters/index.md +0 -104
  62. data/docs/api/adapters/openai.md +0 -134
  63. data/docs/api/history/active-record-adapter.md +0 -275
  64. data/docs/api/history/config.md +0 -284
  65. data/docs/api/history/index.md +0 -128
  66. data/docs/api/history/thread-manager.md +0 -194
  67. data/docs/guides/history.md +0 -359
  68. data/lib/robot_lab/adapters/anthropic.rb +0 -163
  69. data/lib/robot_lab/adapters/base.rb +0 -85
  70. data/lib/robot_lab/adapters/gemini.rb +0 -193
  71. data/lib/robot_lab/adapters/openai.rb +0 -160
  72. data/lib/robot_lab/adapters/registry.rb +0 -81
  73. data/lib/robot_lab/errors.rb +0 -70
  74. data/lib/robot_lab/history/active_record_adapter.rb +0 -146
  75. data/lib/robot_lab/history/config.rb +0 -115
  76. data/lib/robot_lab/history/thread_manager.rb +0 -93
  77. data/lib/robot_lab/robotic_model.rb +0 -324
@@ -1,359 +0,0 @@
1
- # Conversation History
2
-
3
- Persist and restore conversation threads across sessions.
4
-
5
- ## Overview
6
-
7
- History allows you to:
8
-
9
- - Save conversation results to a database
10
- - Restore previous conversations
11
- - Continue multi-turn interactions
12
- - Maintain context across sessions
13
-
14
- ## Configuration
15
-
16
- ### History Config
17
-
18
- Configure history with callbacks:
19
-
20
- ```ruby
21
- history_config = RobotLab::History::Config.new(
22
- create_thread: ->(state:, input:, **) {
23
- # Create a new thread, return thread_id
24
- { thread_id: SecureRandom.uuid }
25
- },
26
-
27
- get: ->(thread_id:, **) {
28
- # Retrieve history for thread
29
- # Return Array<RobotResult>
30
- []
31
- },
32
-
33
- append_user_message: ->(thread_id:, message:, **) {
34
- # Optional: Store user message
35
- },
36
-
37
- append_results: ->(thread_id:, new_results:, **) {
38
- # Store new results
39
- }
40
- )
41
- ```
42
-
43
- ### Apply to Network
44
-
45
- ```ruby
46
- network = RobotLab.create_network do
47
- name "persistent_chat"
48
- history history_config
49
- end
50
- ```
51
-
52
- ## Callback Reference
53
-
54
- ### create_thread
55
-
56
- Called when starting a new conversation:
57
-
58
- ```ruby
59
- create_thread: ->(state:, input:, **kwargs) {
60
- # state - Current State object
61
- # input - UserMessage or string
62
- # kwargs - Additional context
63
-
64
- thread = Thread.create!(
65
- initial_input: input.to_s,
66
- user_id: state.data[:user_id]
67
- )
68
-
69
- { thread_id: thread.id.to_s } # Must return hash with :thread_id
70
- }
71
- ```
72
-
73
- ### get
74
-
75
- Called to retrieve existing history:
76
-
77
- ```ruby
78
- get: ->(thread_id:, **kwargs) {
79
- # thread_id - The thread identifier
80
- # kwargs - Additional context
81
-
82
- Result.where(thread_id: thread_id)
83
- .order(:created_at)
84
- .map { |r| deserialize_result(r) }
85
-
86
- # Must return Array<RobotResult>
87
- }
88
- ```
89
-
90
- ### append_user_message (Optional)
91
-
92
- Called when a user message is added:
93
-
94
- ```ruby
95
- append_user_message: ->(thread_id:, message:, **kwargs) {
96
- # thread_id - The thread identifier
97
- # message - UserMessage object
98
-
99
- Message.create!(
100
- thread_id: thread_id,
101
- content: message.content,
102
- metadata: message.metadata
103
- )
104
- }
105
- ```
106
-
107
- ### append_results
108
-
109
- Called after robots finish:
110
-
111
- ```ruby
112
- append_results: ->(thread_id:, new_results:, **kwargs) {
113
- # thread_id - The thread identifier
114
- # new_results - Array<RobotResult>
115
-
116
- new_results.each do |result|
117
- Result.create!(
118
- thread_id: thread_id,
119
- robot_name: result.robot_name,
120
- output_data: serialize_output(result.output),
121
- stop_reason: result.stop_reason
122
- )
123
- end
124
- }
125
- ```
126
-
127
- ## ActiveRecord Adapter
128
-
129
- RobotLab includes a built-in ActiveRecord adapter:
130
-
131
- ```ruby
132
- adapter = RobotLab::History::ActiveRecordAdapter.new(
133
- thread_model: RobotLabThread,
134
- result_model: RobotLabResult
135
- )
136
-
137
- network = RobotLab.create_network do
138
- history adapter.to_config
139
- end
140
- ```
141
-
142
- ### Required Models
143
-
144
- ```ruby title="app/models/robot_lab_thread.rb"
145
- class RobotLabThread < ApplicationRecord
146
- has_many :results, class_name: "RobotLabResult", foreign_key: :thread_id
147
-
148
- # Required columns:
149
- # - thread_id: string
150
- # - initial_input: text
151
- # - input_metadata: jsonb
152
- # - state_data: jsonb
153
- # - last_user_message: text
154
- # - last_user_message_at: datetime
155
- end
156
- ```
157
-
158
- ```ruby title="app/models/robot_lab_result.rb"
159
- class RobotLabResult < ApplicationRecord
160
- belongs_to :thread, class_name: "RobotLabThread", foreign_key: :thread_id
161
-
162
- # Required columns:
163
- # - thread_id: string
164
- # - robot_name: string
165
- # - sequence_number: integer
166
- # - output_data: jsonb
167
- # - tool_calls_data: jsonb
168
- # - stop_reason: string
169
- # - checksum: string
170
- end
171
- ```
172
-
173
- ## Using Thread IDs
174
-
175
- ### Start New Thread
176
-
177
- ```ruby
178
- state = RobotLab.create_state(message: "Hello!")
179
- result = network.run(state: state)
180
-
181
- # Thread ID is assigned automatically
182
- thread_id = state.thread_id
183
- ```
184
-
185
- ### Continue Existing Thread
186
-
187
- ```ruby
188
- # Option 1: Via UserMessage
189
- message = RobotLab::UserMessage.new(
190
- "Continue our conversation",
191
- thread_id: existing_thread_id
192
- )
193
- state = RobotLab.create_state(message: message)
194
-
195
- # Option 2: Direct assignment
196
- state = RobotLab.create_state(message: "Continue")
197
- state.thread_id = existing_thread_id
198
-
199
- # History is automatically loaded
200
- result = network.run(state: state)
201
- ```
202
-
203
- ## ThreadManager
204
-
205
- For programmatic control:
206
-
207
- ```ruby
208
- manager = RobotLab::History::ThreadManager.new(history_config)
209
-
210
- # Create thread
211
- thread_id = manager.create_thread(state: state, input: message)
212
-
213
- # Load history
214
- results = manager.get_history(thread_id)
215
-
216
- # Save state
217
- manager.save_state(thread_id: thread_id, state: state, since_index: 5)
218
- ```
219
-
220
- ## Serialization
221
-
222
- ### RobotResult
223
-
224
- Results are serialized via `export`:
225
-
226
- ```ruby
227
- result.export
228
- # => {
229
- # robot_name: "assistant",
230
- # output: [...],
231
- # tool_calls: [...],
232
- # stop_reason: "stop",
233
- # id: "...",
234
- # created_at: "..."
235
- # }
236
- ```
237
-
238
- ### Messages
239
-
240
- Messages serialize to hashes:
241
-
242
- ```ruby
243
- message.to_h
244
- # => {
245
- # type: "text",
246
- # role: "assistant",
247
- # content: "Hello!",
248
- # stop_reason: "stop"
249
- # }
250
- ```
251
-
252
- ### Restore from hash
253
-
254
- ```ruby
255
- RobotLab::Message.from_hash(hash)
256
- ```
257
-
258
- ## Patterns
259
-
260
- ### Redis-Based History
261
-
262
- ```ruby
263
- history_config = History::Config.new(
264
- create_thread: ->(state:, input:, **) {
265
- thread_id = SecureRandom.uuid
266
- Redis.current.hset("threads", thread_id, input.to_s)
267
- { thread_id: thread_id }
268
- },
269
-
270
- get: ->(thread_id:, **) {
271
- data = Redis.current.lrange("results:#{thread_id}", 0, -1)
272
- data.map { |json| deserialize_result(JSON.parse(json)) }
273
- },
274
-
275
- append_results: ->(thread_id:, new_results:, **) {
276
- new_results.each do |result|
277
- Redis.current.rpush("results:#{thread_id}", result.export.to_json)
278
- end
279
- }
280
- )
281
- ```
282
-
283
- ### Custom Storage
284
-
285
- ```ruby
286
- class CustomHistoryAdapter
287
- def initialize(storage)
288
- @storage = storage
289
- end
290
-
291
- def to_config
292
- History::Config.new(
293
- create_thread: method(:create_thread),
294
- get: method(:get),
295
- append_results: method(:append_results)
296
- )
297
- end
298
-
299
- private
300
-
301
- def create_thread(state:, input:, **)
302
- id = @storage.create_conversation(input: input.to_s)
303
- { thread_id: id }
304
- end
305
-
306
- def get(thread_id:, **)
307
- @storage.fetch_results(thread_id)
308
- end
309
-
310
- def append_results(thread_id:, new_results:, **)
311
- @storage.store_results(thread_id, new_results)
312
- end
313
- end
314
- ```
315
-
316
- ## Best Practices
317
-
318
- ### 1. Handle Missing Threads
319
-
320
- ```ruby
321
- get: ->(thread_id:, **) {
322
- thread = Thread.find_by(thread_id: thread_id)
323
- return [] unless thread
324
-
325
- thread.results.order(:created_at).map(&:to_robot_result)
326
- }
327
- ```
328
-
329
- ### 2. Index for Performance
330
-
331
- ```sql
332
- CREATE INDEX idx_results_thread_id ON robot_lab_results(thread_id);
333
- CREATE INDEX idx_results_created_at ON robot_lab_results(created_at);
334
- ```
335
-
336
- ### 3. Clean Up Old Threads
337
-
338
- ```ruby
339
- # Periodic cleanup job
340
- Thread.where("updated_at < ?", 30.days.ago).destroy_all
341
- ```
342
-
343
- ### 4. Limit History Size
344
-
345
- ```ruby
346
- get: ->(thread_id:, **) {
347
- Result.where(thread_id: thread_id)
348
- .order(created_at: :desc)
349
- .limit(50) # Last 50 exchanges
350
- .reverse
351
- .map(&:to_robot_result)
352
- }
353
- ```
354
-
355
- ## Next Steps
356
-
357
- - [Memory System](memory.md) - In-memory data sharing
358
- - [State Management](../architecture/state-management.md) - State details
359
- - [API Reference: History](../api/history/index.md) - Complete API
@@ -1,163 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- module Adapters
5
- # Adapter for Anthropic Claude models
6
- #
7
- # Handles Anthropic-specific API conventions:
8
- # - System message as top-level parameter (not in messages array)
9
- # - Tool use/result format differences
10
- # - Content block structure
11
- #
12
- class Anthropic < Base
13
- # Creates a new Anthropic adapter instance.
14
- def initialize
15
- super(:anthropic)
16
- end
17
-
18
- # Format messages for Anthropic API
19
- #
20
- # Anthropic requires system message at top level, not in messages array.
21
- # Also handles tool_use and tool_result message formats.
22
- #
23
- # @param messages [Array<Message>]
24
- # @return [Array<Hash>]
25
- #
26
- def format_messages(messages)
27
- conversation_messages(messages).map do |msg|
28
- format_single_message(msg)
29
- end
30
- end
31
-
32
- # Parse Anthropic response into internal messages
33
- #
34
- # @param response [RubyLLM::Response] ruby_llm response object
35
- # @return [Array<Message>]
36
- #
37
- def parse_response(response)
38
- messages = []
39
-
40
- # Handle text content
41
- if response.content && !response.content.empty?
42
- messages << TextMessage.new(
43
- role: "assistant",
44
- content: response.content,
45
- stop_reason: response.tool_calls&.any? ? "tool" : "stop"
46
- )
47
- end
48
-
49
- # Handle tool calls
50
- if response.tool_calls&.any?
51
- tool_messages = response.tool_calls.map do |id, tool_call|
52
- ToolMessage.new(
53
- id: id,
54
- name: tool_call.name,
55
- input: parse_tool_arguments(tool_call.arguments)
56
- )
57
- end
58
-
59
- messages << ToolCallMessage.new(
60
- role: "assistant",
61
- tools: tool_messages,
62
- stop_reason: "tool"
63
- )
64
- end
65
-
66
- messages
67
- end
68
-
69
- # Format tools for Anthropic
70
- #
71
- # @param tools [Array<Tool>]
72
- # @return [Array<Hash>]
73
- #
74
- def format_tools(tools)
75
- tools.map do |tool|
76
- schema = tool.to_json_schema
77
- {
78
- name: schema[:name],
79
- description: schema[:description],
80
- input_schema: schema[:parameters] || { type: "object", properties: {} }
81
- }
82
- end
83
- end
84
-
85
- # Anthropic tool choice format
86
- #
87
- # @param choice [String, Symbol]
88
- # @return [Hash]
89
- #
90
- def format_tool_choice(choice)
91
- case choice.to_s
92
- when "auto" then { type: "auto" }
93
- when "any" then { type: "any" }
94
- else { type: "tool", name: choice.to_s }
95
- end
96
- end
97
-
98
- private
99
-
100
- def format_single_message(msg)
101
- case msg
102
- when TextMessage
103
- { role: msg.role, content: msg.content }
104
- when ToolCallMessage
105
- {
106
- role: "assistant",
107
- content: msg.tools.map do |tool|
108
- {
109
- type: "tool_use",
110
- id: tool.id,
111
- name: tool.name,
112
- input: tool.input
113
- }
114
- end
115
- }
116
- when ToolResultMessage
117
- {
118
- role: "user",
119
- content: [
120
- {
121
- type: "tool_result",
122
- tool_use_id: msg.tool.id,
123
- content: format_tool_result_content(msg.content)
124
- }
125
- ]
126
- }
127
- else
128
- { role: msg.role, content: msg.content.to_s }
129
- end
130
- end
131
-
132
- def format_tool_result_content(content)
133
- case content
134
- when Hash
135
- if content[:error]
136
- JSON.generate(content)
137
- elsif content[:data]
138
- content[:data].is_a?(String) ? content[:data] : JSON.generate(content[:data])
139
- else
140
- JSON.generate(content)
141
- end
142
- else
143
- content.to_s
144
- end
145
- end
146
-
147
- def parse_tool_arguments(arguments)
148
- case arguments
149
- when String
150
- begin
151
- JSON.parse(arguments, symbolize_names: true)
152
- rescue JSON::ParserError
153
- { raw: arguments }
154
- end
155
- when Hash
156
- arguments.transform_keys(&:to_sym)
157
- else
158
- arguments || {}
159
- end
160
- end
161
- end
162
- end
163
- end
@@ -1,85 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- module Adapters
5
- # Base adapter interface for LLM providers
6
- #
7
- # Adapters handle provider-specific message formatting and response parsing.
8
- # Each provider (Anthropic, OpenAI, Gemini) has different API conventions
9
- # that the adapter normalizes.
10
- #
11
- # @abstract Subclass and implement {#format_messages} and {#parse_response}
12
- #
13
- class Base
14
- # @!attribute [r] provider
15
- # @return [Symbol] the provider name
16
- attr_reader :provider
17
-
18
- # Creates a new adapter instance.
19
- #
20
- # @param provider [Symbol] the provider name
21
- def initialize(provider)
22
- @provider = provider
23
- end
24
-
25
- # Format internal messages for the provider's API
26
- #
27
- # @param messages [Array<Message>] Internal message format
28
- # @return [Array<Hash>] Provider-specific message format
29
- #
30
- def format_messages(messages)
31
- raise NotImplementedError, "#{self.class}#format_messages must be implemented"
32
- end
33
-
34
- # Parse provider response into internal message format
35
- #
36
- # @param response [Object] Provider-specific response
37
- # @return [Array<Message>] Internal message format
38
- #
39
- def parse_response(response)
40
- raise NotImplementedError, "#{self.class}#parse_response must be implemented"
41
- end
42
-
43
- # Format tools for the provider's function calling API
44
- #
45
- # @param tools [Array<Tool>] Internal tool definitions
46
- # @return [Array<Hash>] Provider-specific tool format
47
- #
48
- def format_tools(tools)
49
- tools.map(&:to_json_schema)
50
- end
51
-
52
- # Format tool choice for the provider
53
- #
54
- # @param choice [String, Symbol] "auto", "any", or specific tool name
55
- # @return [Object] Provider-specific tool choice
56
- #
57
- def format_tool_choice(choice)
58
- case choice.to_s
59
- when "auto" then "auto"
60
- when "any" then "required"
61
- else { type: "function", function: { name: choice.to_s } }
62
- end
63
- end
64
-
65
- # Extract system message from messages array
66
- #
67
- # @param messages [Array<Message>]
68
- # @return [String, nil]
69
- #
70
- def extract_system_message(messages)
71
- system_msg = messages.find(&:system?)
72
- system_msg&.content
73
- end
74
-
75
- # Filter out system messages
76
- #
77
- # @param messages [Array<Message>]
78
- # @return [Array<Message>]
79
- #
80
- def conversation_messages(messages)
81
- messages.reject(&:system?)
82
- end
83
- end
84
- end
85
- end