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,193 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- module Adapters
5
- # Adapter for Google Gemini models
6
- #
7
- # Handles Gemini-specific API conventions:
8
- # - Role mapping (assistant -> model)
9
- # - Contents/parts array structure
10
- # - functionCall/functionResponse format
11
- #
12
- class Gemini < Base
13
- # Creates a new Gemini adapter instance.
14
- def initialize
15
- super(:gemini)
16
- end
17
-
18
- # Format messages for Gemini API
19
- #
20
- # Gemini uses "model" role instead of "assistant" and structures
21
- # content as parts arrays.
22
- #
23
- # @param messages [Array<Message>]
24
- # @return [Array<Hash>]
25
- #
26
- def format_messages(messages)
27
- # Gemini handles system messages differently - as system_instruction
28
- conversation_messages(messages).map { |msg| format_single_message(msg) }
29
- end
30
-
31
- # Parse Gemini response into internal messages
32
- #
33
- # @param response [RubyLLM::Response]
34
- # @return [Array<Message>]
35
- #
36
- def parse_response(response)
37
- messages = []
38
-
39
- # Handle text content
40
- if response.content && !response.content.empty?
41
- messages << TextMessage.new(
42
- role: "assistant",
43
- content: response.content,
44
- stop_reason: response.tool_calls&.any? ? "tool" : "stop"
45
- )
46
- end
47
-
48
- # Handle function calls
49
- if response.tool_calls&.any?
50
- tool_messages = response.tool_calls.map do |id, tool_call|
51
- ToolMessage.new(
52
- id: id,
53
- name: tool_call.name,
54
- input: parse_tool_arguments(tool_call.arguments)
55
- )
56
- end
57
-
58
- messages << ToolCallMessage.new(
59
- role: "assistant",
60
- tools: tool_messages,
61
- stop_reason: "tool"
62
- )
63
- end
64
-
65
- messages
66
- end
67
-
68
- # Format tools for Gemini function declarations
69
- #
70
- # Gemini doesn't support additionalProperties in schemas
71
- #
72
- # @param tools [Array<Tool>]
73
- # @return [Array<Hash>]
74
- #
75
- def format_tools(tools)
76
- tools.map do |tool|
77
- schema = tool.to_json_schema
78
- params = clean_schema_for_gemini(schema[:parameters] || { type: "object", properties: {} })
79
- {
80
- name: schema[:name],
81
- description: schema[:description],
82
- parameters: params
83
- }
84
- end
85
- end
86
-
87
- # Gemini tool choice format
88
- #
89
- # @param choice [String, Symbol]
90
- # @return [Hash]
91
- #
92
- def format_tool_choice(choice)
93
- case choice.to_s
94
- when "auto" then { mode: "AUTO" }
95
- when "any" then { mode: "ANY" }
96
- when "none" then { mode: "NONE" }
97
- else { mode: "ANY", allowed_function_names: [choice.to_s] }
98
- end
99
- end
100
-
101
- private
102
-
103
- def format_single_message(msg)
104
- role = gemini_role(msg.role)
105
-
106
- case msg
107
- when TextMessage
108
- {
109
- role: role,
110
- parts: [{ text: msg.content }]
111
- }
112
- when ToolCallMessage
113
- {
114
- role: "model",
115
- parts: msg.tools.map do |tool|
116
- {
117
- functionCall: {
118
- name: tool.name,
119
- args: tool.input
120
- }
121
- }
122
- end
123
- }
124
- when ToolResultMessage
125
- {
126
- role: "user",
127
- parts: [
128
- {
129
- functionResponse: {
130
- name: msg.tool.name,
131
- response: format_tool_result_content(msg.content)
132
- }
133
- }
134
- ]
135
- }
136
- else
137
- { role: role, parts: [{ text: msg.content.to_s }] }
138
- end
139
- end
140
-
141
- def gemini_role(role)
142
- case role.to_s
143
- when "assistant" then "model"
144
- when "system" then "user" # Gemini handles system as system_instruction
145
- else role.to_s
146
- end
147
- end
148
-
149
- def format_tool_result_content(content)
150
- case content
151
- when Hash
152
- content
153
- when String
154
- { result: content }
155
- else
156
- { result: content.to_s }
157
- end
158
- end
159
-
160
- def parse_tool_arguments(arguments)
161
- case arguments
162
- when String
163
- begin
164
- JSON.parse(arguments, symbolize_names: true)
165
- rescue JSON::ParserError
166
- { raw: arguments }
167
- end
168
- when Hash
169
- arguments.transform_keys(&:to_sym)
170
- else
171
- arguments || {}
172
- end
173
- end
174
-
175
- # Remove additionalProperties which Gemini doesn't support
176
- def clean_schema_for_gemini(schema)
177
- return schema unless schema.is_a?(Hash)
178
-
179
- cleaned = schema.dup
180
- cleaned.delete(:additionalProperties)
181
- cleaned.delete("additionalProperties")
182
-
183
- if cleaned[:properties]
184
- cleaned[:properties] = cleaned[:properties].transform_values do |prop|
185
- clean_schema_for_gemini(prop)
186
- end
187
- end
188
-
189
- cleaned
190
- end
191
- end
192
- end
193
- end
@@ -1,160 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- module Adapters
5
- # Adapter for OpenAI GPT models
6
- #
7
- # Handles OpenAI-specific API conventions:
8
- # - Function calling format
9
- # - Strict mode for structured outputs
10
- # - finish_reason to stop_reason mapping
11
- #
12
- class OpenAI < Base
13
- # Creates a new OpenAI adapter instance.
14
- def initialize
15
- super(:openai)
16
- end
17
-
18
- # Format messages for OpenAI API
19
- #
20
- # @param messages [Array<Message>]
21
- # @return [Array<Hash>]
22
- #
23
- def format_messages(messages)
24
- messages.map { |msg| format_single_message(msg) }
25
- end
26
-
27
- # Parse OpenAI response into internal messages
28
- #
29
- # @param response [RubyLLM::Response]
30
- # @return [Array<Message>]
31
- #
32
- def parse_response(response)
33
- messages = []
34
-
35
- # Handle text content
36
- if response.content && !response.content.empty?
37
- messages << TextMessage.new(
38
- role: "assistant",
39
- content: response.content,
40
- stop_reason: response.tool_calls&.any? ? "tool" : "stop"
41
- )
42
- end
43
-
44
- # Handle tool calls
45
- if response.tool_calls&.any?
46
- tool_messages = response.tool_calls.map do |id, tool_call|
47
- ToolMessage.new(
48
- id: id,
49
- name: tool_call.name,
50
- input: parse_tool_arguments(tool_call.arguments)
51
- )
52
- end
53
-
54
- messages << ToolCallMessage.new(
55
- role: "assistant",
56
- tools: tool_messages,
57
- stop_reason: "tool"
58
- )
59
- end
60
-
61
- messages
62
- end
63
-
64
- # Format tools for OpenAI function calling
65
- #
66
- # @param tools [Array<Tool>]
67
- # @return [Array<Hash>]
68
- #
69
- def format_tools(tools)
70
- tools.map do |tool|
71
- schema = tool.to_json_schema
72
- strict = tool.provider_params[:strict]
73
- {
74
- type: "function",
75
- function: {
76
- name: schema[:name],
77
- description: schema[:description],
78
- parameters: schema[:parameters] || { type: "object", properties: {} },
79
- strict: strict.nil? ? true : strict
80
- }.compact
81
- }
82
- end
83
- end
84
-
85
- # OpenAI tool choice format
86
- #
87
- # @param choice [String, Symbol]
88
- # @return [String, Hash]
89
- #
90
- def format_tool_choice(choice)
91
- case choice.to_s
92
- when "auto" then "auto"
93
- when "any" then "required"
94
- when "none" then "none"
95
- else { type: "function", function: { name: choice.to_s } }
96
- end
97
- end
98
-
99
- private
100
-
101
- def format_single_message(msg)
102
- case msg
103
- when TextMessage
104
- { role: msg.role, content: msg.content }
105
- when ToolCallMessage
106
- {
107
- role: "assistant",
108
- content: nil,
109
- tool_calls: msg.tools.map do |tool|
110
- {
111
- id: tool.id,
112
- type: "function",
113
- function: {
114
- name: tool.name,
115
- arguments: JSON.generate(tool.input)
116
- }
117
- }
118
- end
119
- }
120
- when ToolResultMessage
121
- {
122
- role: "tool",
123
- tool_call_id: msg.tool.id,
124
- content: format_tool_result_content(msg.content)
125
- }
126
- else
127
- { role: msg.role, content: msg.content.to_s }
128
- end
129
- end
130
-
131
- def format_tool_result_content(content)
132
- case content
133
- when Hash
134
- JSON.generate(content)
135
- when String
136
- content
137
- else
138
- content.to_s
139
- end
140
- end
141
-
142
- def parse_tool_arguments(arguments)
143
- case arguments
144
- when String
145
- # Handle OpenAI's backtick wrapping quirk
146
- cleaned = arguments.gsub(/\A```(?:json)?\n?/, "").gsub(/\n?```\z/, "")
147
- begin
148
- JSON.parse(cleaned, symbolize_names: true)
149
- rescue JSON::ParserError
150
- { raw: arguments }
151
- end
152
- when Hash
153
- arguments.transform_keys(&:to_sym)
154
- else
155
- arguments || {}
156
- end
157
- end
158
- end
159
- end
160
- end
@@ -1,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- module Adapters
5
- # Registry for looking up provider adapters
6
- #
7
- # Maps provider symbols to their adapter classes.
8
- #
9
- # @example
10
- # adapter = Registry.for(:anthropic)
11
- # adapter.format_messages(messages)
12
- #
13
- module Registry
14
- # @return [Hash<Symbol, Class>] mapping of provider symbols to adapter classes
15
- ADAPTERS = {
16
- anthropic: Anthropic,
17
- openai: OpenAI,
18
- gemini: Gemini,
19
- # Azure uses OpenAI adapter
20
- azure_openai: OpenAI,
21
- # Grok uses OpenAI adapter
22
- grok: OpenAI,
23
- # Ollama uses OpenAI adapter
24
- ollama: OpenAI,
25
- # OpenRouter uses OpenAI adapter
26
- openrouter: OpenAI,
27
- # Bedrock uses Anthropic adapter
28
- bedrock: Anthropic,
29
- # VertexAI uses Gemini adapter
30
- vertexai: Gemini
31
- }.freeze
32
-
33
- class << self
34
- # Get adapter for a provider
35
- #
36
- # @param provider [Symbol, String] Provider name
37
- # @return [Base] Adapter instance
38
- # @raise [ArgumentError] If provider not found
39
- #
40
- def for(provider)
41
- provider_sym = provider.to_s.downcase.gsub("-", "_").to_sym
42
- adapter_class = ADAPTERS[provider_sym]
43
-
44
- unless adapter_class
45
- raise ArgumentError, "Unknown provider: #{provider}. " \
46
- "Available providers: #{available.join(', ')}"
47
- end
48
-
49
- adapter_class.new
50
- end
51
-
52
- # List available providers
53
- #
54
- # @return [Array<Symbol>]
55
- #
56
- def available
57
- ADAPTERS.keys
58
- end
59
-
60
- # Check if provider is supported
61
- #
62
- # @param provider [Symbol, String]
63
- # @return [Boolean]
64
- #
65
- def supports?(provider)
66
- provider_sym = provider.to_s.downcase.gsub("-", "_").to_sym
67
- ADAPTERS.key?(provider_sym)
68
- end
69
-
70
- # Register a custom adapter
71
- #
72
- # @param provider [Symbol] Provider name
73
- # @param adapter_class [Class] Adapter class
74
- #
75
- def register(provider, adapter_class)
76
- ADAPTERS[provider.to_sym] = adapter_class
77
- end
78
- end
79
- end
80
- end
81
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- # Error serialization utilities
5
- #
6
- # Provides methods to serialize Ruby exceptions into a format
7
- # suitable for tool results and logging.
8
- #
9
- module Errors
10
- class << self
11
- # Serialize an exception to a hash
12
- #
13
- # @param error [Exception] The error to serialize
14
- # @param include_backtrace [Boolean] Whether to include backtrace
15
- # @return [Hash] Serialized error
16
- #
17
- def serialize(error, include_backtrace: false)
18
- result = {
19
- type: error.class.name,
20
- message: error.message
21
- }
22
-
23
- if include_backtrace && error.backtrace
24
- result[:backtrace] = error.backtrace.first(10)
25
- end
26
-
27
- if error.cause
28
- result[:cause] = serialize(error.cause, include_backtrace: include_backtrace)
29
- end
30
-
31
- result
32
- end
33
-
34
- # Deserialize an error hash back to an exception
35
- #
36
- # @param hash [Hash] Serialized error
37
- # @return [StandardError]
38
- #
39
- def deserialize(hash)
40
- hash = hash.transform_keys(&:to_sym)
41
- klass = begin
42
- Object.const_get(hash[:type])
43
- rescue NameError
44
- StandardError
45
- end
46
- klass.new(hash[:message])
47
- end
48
-
49
- # Format error for display
50
- #
51
- # @param error [Exception] The error
52
- # @return [String]
53
- #
54
- def format(error)
55
- "[#{error.class.name}] #{error.message}"
56
- end
57
-
58
- # Wrap a block and return error hash on failure
59
- #
60
- # @yield Block to execute
61
- # @return [Hash] { data: result } or { error: serialized_error }
62
- #
63
- def capture
64
- { data: yield }
65
- rescue StandardError => e
66
- { error: serialize(e) }
67
- end
68
- end
69
- end
70
- end
@@ -1,146 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- module History
5
- # ActiveRecord-based history persistence adapter
6
- #
7
- # Provides thread and result storage using ActiveRecord models.
8
- # Requires Rails or standalone ActiveRecord setup.
9
- #
10
- # @example
11
- # adapter = ActiveRecordAdapter.new(
12
- # thread_model: RobotLabThread,
13
- # result_model: RobotLabResult
14
- # )
15
- #
16
- # config = adapter.to_config
17
- # network = RobotLab.create_network(history: config)
18
- #
19
- class ActiveRecordAdapter
20
- # @!attribute [r] thread_model
21
- # @return [Class] ActiveRecord model class for threads
22
- # @!attribute [r] result_model
23
- # @return [Class] ActiveRecord model class for results
24
- attr_reader :thread_model, :result_model
25
-
26
- # Initialize adapter with ActiveRecord models
27
- #
28
- # @param thread_model [Class] ActiveRecord model for threads
29
- # @param result_model [Class] ActiveRecord model for results
30
- #
31
- def initialize(thread_model:, result_model:)
32
- @thread_model = thread_model
33
- @result_model = result_model
34
- end
35
-
36
- # Create a new thread
37
- #
38
- # @param state [State] Current state
39
- # @param input [String, UserMessage] Initial input
40
- # @return [Hash] Thread ID and metadata
41
- #
42
- def create_thread(state:, input:, **)
43
- input_content = input.is_a?(UserMessage) ? input.content : input.to_s
44
- input_metadata = input.is_a?(UserMessage) ? input.metadata : {}
45
-
46
- thread = @thread_model.create!(
47
- session_id: SecureRandom.uuid,
48
- initial_input: input_content,
49
- input_metadata: input_metadata,
50
- state_data: state.data.to_h
51
- )
52
-
53
- { session_id: thread.session_id, created_at: thread.created_at }
54
- end
55
-
56
- # Retrieve results for a thread
57
- #
58
- # @param session_id [String] Thread identifier
59
- # @return [Array<RobotResult>] History of results
60
- #
61
- def get(session_id:, **)
62
- @result_model
63
- .where(session_id: session_id)
64
- .order(:sequence_number, :created_at)
65
- .map { |record| deserialize_result(record) }
66
- end
67
-
68
- # Append user message to thread
69
- #
70
- # @param session_id [String] Thread identifier
71
- # @param message [UserMessage] Message to append
72
- #
73
- def append_user_message(session_id:, message:, **)
74
- @thread_model.where(session_id: session_id).update_all(
75
- last_user_message: message.content,
76
- last_user_message_at: Time.current
77
- )
78
- end
79
-
80
- # Append results to thread
81
- #
82
- # @param session_id [String] Thread identifier
83
- # @param new_results [Array<RobotResult>] Results to append
84
- #
85
- def append_results(session_id:, new_results:, **)
86
- base_sequence = @result_model.where(session_id: session_id).maximum(:sequence_number) || 0
87
-
88
- new_results.each_with_index do |result, index|
89
- @result_model.create!(
90
- session_id: session_id,
91
- robot_name: result.robot_name,
92
- sequence_number: base_sequence + index + 1,
93
- output_data: serialize_messages(result.output),
94
- tool_calls_data: serialize_messages(result.tool_calls),
95
- stop_reason: result.stop_reason,
96
- checksum: result.checksum
97
- )
98
- end
99
-
100
- # Update thread timestamp
101
- @thread_model.where(session_id: session_id).update_all(
102
- updated_at: Time.current
103
- )
104
- end
105
-
106
- # Convert adapter to Config object
107
- #
108
- # @return [Config] History configuration
109
- #
110
- def to_config
111
- Config.new(
112
- create_thread: method(:create_thread),
113
- get: method(:get),
114
- append_user_message: method(:append_user_message),
115
- append_results: method(:append_results)
116
- )
117
- end
118
-
119
- private
120
-
121
- def serialize_messages(messages)
122
- messages.map(&:to_h)
123
- end
124
-
125
- def deserialize_result(record)
126
- output = deserialize_messages(record.output_data)
127
- tool_calls = deserialize_messages(record.tool_calls_data)
128
-
129
- RobotResult.new(
130
- robot_name: record.robot_name,
131
- output: output,
132
- tool_calls: tool_calls,
133
- stop_reason: record.stop_reason
134
- )
135
- end
136
-
137
- def deserialize_messages(data)
138
- return [] unless data
139
-
140
- data.map do |msg_hash|
141
- Message.from_hash(msg_hash.symbolize_keys)
142
- end
143
- end
144
- end
145
- end
146
- end