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
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ # Typed message envelope for bus-based robot communication.
5
+ #
6
+ # RobotMessage is a Data class (immutable value object) that wraps
7
+ # content sent between robots via a TypedBus channel.
8
+ #
9
+ # @example Creating a new message
10
+ # msg = RobotMessage.build(id: 1, from: "alice", content: "Hello")
11
+ # msg.key #=> "alice:1"
12
+ # msg.reply? #=> false
13
+ #
14
+ # @example Creating a reply
15
+ # reply = RobotMessage.build(
16
+ # id: 2, from: "bob",
17
+ # content: "Hi back",
18
+ # in_reply_to: "alice:1"
19
+ # )
20
+ # reply.reply? #=> true
21
+ #
22
+ RobotMessage = Data.define(:id, :from, :content, :in_reply_to) do
23
+ # Build a RobotMessage with in_reply_to defaulting to nil.
24
+ #
25
+ # @param id [Integer] per-robot message counter
26
+ # @param from [String] sender's robot name (= channel name)
27
+ # @param content [String, Hash] message payload
28
+ # @param in_reply_to [String, nil] composite key of the message being replied to
29
+ # @return [RobotMessage]
30
+ def self.build(id:, from:, content:, in_reply_to: nil)
31
+ new(id: id, from: from, content: content, in_reply_to: in_reply_to)
32
+ end
33
+
34
+ # Composite identity key: "from:id"
35
+ #
36
+ # @return [String]
37
+ def key = "#{from}:#{id}"
38
+
39
+ # Whether this message is a reply to another message.
40
+ #
41
+ # @return [Boolean]
42
+ def reply? = !in_reply_to.nil?
43
+ end
44
+ end
@@ -135,6 +135,7 @@ module RobotLab
135
135
  def last_text_content
136
136
  output.reverse.find(&:text?)&.content
137
137
  end
138
+ alias_method :reply, :last_text_content
138
139
 
139
140
  # Check if result contains tool calls
140
141
  #
@@ -244,7 +244,7 @@ module RobotLab
244
244
  when /^llama/, /^mistral/, /^mixtral/
245
245
  :ollama
246
246
  else
247
- RobotLab.configuration.default_provider
247
+ RobotLab.config.ruby_llm.provider
248
248
  end
249
249
  end
250
250
  end
@@ -56,7 +56,7 @@ module RobotLab
56
56
  begin
57
57
  @publish.call(chunk)
58
58
  rescue StandardError => e
59
- RobotLab.configuration.logger.warn("Streaming error: #{e.message}")
59
+ RobotLab.config.logger&.warn("Streaming error: #{e.message}")
60
60
  end
61
61
 
62
62
  chunk
@@ -1,222 +1,158 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RobotLab
4
- # Defines a tool/function that robots can use
4
+ # A tool that robots can use, built on RubyLLM::Tool.
5
5
  #
6
- # Tools are capabilities that robots can invoke during execution.
7
- # They have a name, description, parameter schema, and handler.
6
+ # Provides two patterns for defining tools:
8
7
  #
9
- # @example Simple tool
10
- # tool = Tool.new(
11
- # name: "get_time",
12
- # description: "Get the current time",
13
- # handler: -> (_input, **_opts) { Time.now.to_s }
14
- # )
8
+ # 1. **Subclass pattern** — for reusable, robot-aware tools:
9
+ #
10
+ # class GetWeather < RobotLab::Tool
11
+ # description "Get weather for a location"
12
+ # param :location, type: "string", desc: "City name"
15
13
  #
16
- # @example Tool with parameters (using ruby_llm-schema)
17
- # class WeatherParams < RubyLLM::Schema
18
- # string :location, description: "City name"
19
- # string :unit, enum: %w[celsius fahrenheit], required: false
14
+ # def execute(location:)
15
+ # WeatherService.fetch(location)
16
+ # end
20
17
  # end
21
18
  #
22
- # tool = Tool.new(
23
- # name: "get_weather",
24
- # description: "Get weather for a location",
25
- # parameters: WeatherParams,
26
- # handler: ->(input, **opts) {
27
- # # input[:location], input[:unit] are validated
28
- # fetch_weather(input[:location], input[:unit] || "celsius")
29
- # }
30
- # )
19
+ # 2. **Factory pattern** — for dynamic/inline tools:
20
+ #
21
+ # tool = RobotLab::Tool.create(
22
+ # name: "get_time",
23
+ # description: "Get the current time"
24
+ # ) { |args| Time.now.to_s }
31
25
  #
32
- class Tool
33
- # @!attribute [r] name
34
- # @return [String] the unique identifier for the tool
35
- # @!attribute [r] description
36
- # @return [String, nil] a description of what the tool does
37
- # @!attribute [r] parameters
38
- # @return [Class, Hash, nil] the parameter schema (RubyLLM::Schema or JSON Schema hash)
39
- # @!attribute [r] handler
40
- # @return [Proc, nil] the callable that executes the tool logic
26
+ # Subclasses have access to the owning +robot+ via an accessor,
27
+ # enabling tools that modify their robot's state (temperature,
28
+ # system prompt, spawning, etc.).
29
+ #
30
+ class Tool < RubyLLM::Tool
31
+ # @!attribute [rw] robot
32
+ # @return [Robot, nil] the robot that owns this tool
33
+ attr_accessor :robot
34
+
41
35
  # @!attribute [r] mcp
42
36
  # @return [String, nil] the MCP server name if this is an MCP-provided tool
43
- # @!attribute [r] strict
44
- # @return [Boolean, nil] whether strict mode is enabled
45
- attr_reader :name, :description, :parameters, :handler, :mcp, :strict
37
+ attr_reader :mcp
46
38
 
47
39
  # Creates a new Tool instance.
48
40
  #
49
- # @param name [String] the unique identifier for the tool
50
- # @param description [String, nil] a description of what the tool does
51
- # @param parameters [Class, Hash, nil] parameter schema (RubyLLM::Schema or JSON Schema)
52
- # @param handler [Proc, nil] the callable that executes the tool logic
53
- # @param mcp [String, nil] MCP server name if this is an MCP tool
54
- # @param strict [Boolean, nil] whether strict mode is enabled
55
- # @yield [input, **opts] optional block as handler
56
- #
57
- # @example Tool with block handler
58
- # Tool.new(name: "greet", description: "Greet user") do |input, **opts|
59
- # "Hello, #{input[:name]}!"
60
- # end
61
- def initialize(name:, description: nil, parameters: nil, handler: nil, mcp: nil, strict: nil, &block)
62
- @name = name.to_s
63
- @description = description
64
- @parameters = parameters
65
- @handler = handler || block
66
- @mcp = mcp
67
- @strict = strict
41
+ # @param robot [Robot, nil] the owning robot
42
+ def initialize(robot: nil)
43
+ super()
44
+ @robot = robot
68
45
  end
69
46
 
70
- # Execute the tool with input and context
47
+ # Override name to support explicit names for dynamic/MCP tools.
71
48
  #
72
- # Supports two calling conventions:
73
- # - Direct: tool.call(input, robot: robot, network: network)
74
- # - ruby_llm: tool.call(args) - called without keyword args
75
- #
76
- # @param input [Hash] The input parameters (validated against schema)
77
- # @param robot [Robot, nil] The robot invoking this tool
78
- # @param network [NetworkRun, nil] The network context if running in a network
79
- # @param step [Object, nil] Durable execution step context
80
- # @return [Object] The tool's output
49
+ # @return [String] the tool name
50
+ def name
51
+ defined?(@custom_name) && @custom_name ? @custom_name : super
52
+ end
53
+
54
+ # Check if this is an MCP-provided tool.
81
55
  #
82
- def call(input, robot: nil, network: nil, step: nil)
83
- raise Error, "Tool '#{name}' has no handler defined" unless handler
56
+ # @return [Boolean]
57
+ def mcp?
58
+ !@mcp.nil?
59
+ end
84
60
 
85
- validated_input = validate_input(input)
86
- handler.call(validated_input, robot: robot, network: network, step: step)
87
- rescue Error
88
- raise
89
- rescue StandardError => e
90
- { error: Errors.serialize(e) }
61
+ # Factory for dynamic tools (MCP wrappers, inline tools).
62
+ #
63
+ # @param name [String, Symbol] the tool name
64
+ # @param description [String, nil] what the tool does
65
+ # @param parameters [Hash, nil] JSON Schema parameter definition
66
+ # @param mcp [String, nil] MCP server name
67
+ # @param robot [Robot, nil] the owning robot
68
+ # @yield [args] block that executes the tool logic
69
+ # @return [Tool] a new tool instance
70
+ #
71
+ # @example Simple factory tool
72
+ # tool = RobotLab::Tool.create(
73
+ # name: "get_time",
74
+ # description: "Get the current time"
75
+ # ) { |args| Time.now.to_s }
76
+ #
77
+ # @example MCP tool wrapper
78
+ # tool = RobotLab::Tool.create(
79
+ # name: "search",
80
+ # description: "Search the web",
81
+ # parameters: { type: "object", properties: { q: { type: "string" } }, required: ["q"] },
82
+ # mcp: "brave_search"
83
+ # ) { |args| mcp_client.call_tool("search", args) }
84
+ #
85
+ def self.create(name:, description: nil, parameters: nil, mcp: nil, robot: nil, &handler)
86
+ desc_text = description
87
+ params_hash = parameters
88
+ block = handler
89
+
90
+ tool_class = Class.new(self) do
91
+ description(desc_text) if desc_text
92
+
93
+ if params_hash.is_a?(Hash) && params_hash[:properties]
94
+ required_list = Array(params_hash[:required]).map(&:to_s)
95
+ params_hash[:properties].each do |pname, pdef|
96
+ param pname.to_sym,
97
+ type: pdef[:type] || "string",
98
+ desc: pdef[:description],
99
+ required: required_list.include?(pname.to_s)
100
+ end
101
+ end
102
+
103
+ define_method(:execute) do |**args|
104
+ block.call(args)
105
+ end
106
+ end
107
+
108
+ instance = tool_class.new(robot: robot)
109
+ instance.instance_variable_set(:@custom_name, name.to_s)
110
+ instance.instance_variable_set(:@mcp, mcp)
111
+ instance
91
112
  end
92
113
 
93
- # Convert to JSON Schema for LLM function calling
114
+ # Convert to JSON Schema for LLM function calling.
115
+ # Used by RobotLab adapters for provider-specific formatting.
94
116
  #
95
117
  # @return [Hash] JSON Schema representation
96
- #
97
118
  def to_json_schema
98
- schema = if parameters.respond_to?(:to_json_schema)
99
- # ruby_llm-schema class
100
- parameters.new.to_json_schema[:schema]
101
- elsif parameters.is_a?(Hash)
102
- # Raw JSON schema
103
- parameters
104
- else
105
- # No parameters
106
- { type: "object", properties: {}, required: [] }
107
- end
108
-
119
+ schema = params_schema || { "type" => "object", "properties" => {}, "required" => [] }
109
120
  {
110
121
  name: name,
111
122
  description: description,
112
- parameters: schema
123
+ parameters: deep_symbolize_keys(schema)
113
124
  }.compact
114
125
  end
115
126
 
116
- # Convert to ruby_llm Tool class for integration
117
- #
118
- # @return [Class] A RubyLLM::Tool subclass
119
- #
120
- def to_ruby_llm_tool
121
- tool = self
122
- Class.new(RubyLLM::Tool) do
123
- description tool.description
124
-
125
- # Define parameters from schema
126
- if tool.parameters.respond_to?(:to_json_schema)
127
- schema = tool.parameters.new.to_json_schema[:schema]
128
- schema[:properties]&.each do |prop_name, prop_def|
129
- param prop_name.to_sym,
130
- type: prop_def[:type],
131
- desc: prop_def[:description],
132
- required: schema[:required]&.include?(prop_name.to_s)
133
- end
134
- elsif tool.parameters.is_a?(Hash) && tool.parameters[:properties]
135
- tool.parameters[:properties].each do |prop_name, prop_def|
136
- param prop_name.to_sym,
137
- type: prop_def[:type] || "string",
138
- desc: prop_def[:description],
139
- required: tool.parameters[:required]&.include?(prop_name.to_s)
140
- end
141
- end
142
-
143
- define_method(:execute) do |**kwargs|
144
- # This will be overridden at runtime with proper context
145
- kwargs
146
- end
147
- end
148
- end
149
-
150
- # Converts the tool to a hash representation.
127
+ # Hash representation.
151
128
  #
152
- # @return [Hash] a hash containing the tool configuration
129
+ # @return [Hash]
153
130
  def to_h
154
131
  {
155
132
  name: name,
156
133
  description: description,
157
- parameters: parameters_to_hash,
158
- mcp: mcp,
159
- strict: strict
134
+ mcp: mcp
160
135
  }.compact
161
136
  end
162
137
 
163
- # Converts the tool to JSON.
138
+ # JSON representation.
164
139
  #
165
140
  # @param args [Array] arguments passed to to_json
166
- # @return [String] JSON representation of the tool
141
+ # @return [String]
167
142
  def to_json(*args)
168
143
  to_h.to_json(*args)
169
144
  end
170
145
 
171
- # Check if this is an MCP-provided tool
172
- #
173
- # @return [Boolean]
174
- #
175
- def mcp?
176
- !mcp.nil?
177
- end
178
-
179
- # Return parameters schema for ruby_llm compatibility
180
- #
181
- # @return [Hash, nil] JSON Schema for tool parameters
182
- #
183
- def params_schema
184
- if parameters.respond_to?(:to_json_schema)
185
- parameters.new.to_json_schema[:schema]
186
- elsif parameters.is_a?(Hash)
187
- parameters
188
- end
189
- end
190
-
191
- # Provider-specific parameters for ruby_llm compatibility
192
- #
193
- # @return [Hash] Empty hash (no provider-specific params)
194
- #
195
- def provider_params
196
- {}
197
- end
198
-
199
146
  private
200
147
 
201
- def validate_input(input)
202
- return input unless parameters
203
-
204
- input = input.transform_keys(&:to_sym) if input.is_a?(Hash)
205
-
206
- if parameters.respond_to?(:new) && parameters.ancestors.include?(defined?(RubyLLM::Schema) ? RubyLLM::Schema : Object)
207
- # Validate with ruby_llm-schema (if available)
208
- # For now, just pass through
209
- input
148
+ def deep_symbolize_keys(obj)
149
+ case obj
150
+ when Hash
151
+ obj.each_with_object({}) { |(k, v), h| h[k.to_sym] = deep_symbolize_keys(v) }
152
+ when Array
153
+ obj.map { |v| deep_symbolize_keys(v) }
210
154
  else
211
- input
212
- end
213
- end
214
-
215
- def parameters_to_hash
216
- if parameters.respond_to?(:to_json_schema)
217
- parameters.new.to_json_schema
218
- elsif parameters.is_a?(Hash)
219
- parameters
155
+ obj
220
156
  end
221
157
  end
222
158
  end
@@ -4,7 +4,7 @@ module RobotLab
4
4
  # Handles hierarchical MCP and tools configuration resolution
5
5
  #
6
6
  # Configuration hierarchy (each level overrides the previous):
7
- # 1. RobotLab.configuration (global)
7
+ # 1. RobotLab.config (global)
8
8
  # 2. Network.new (network scope)
9
9
  # 3. Robot.new (robot definition scope)
10
10
  # 4. robot.run (runtime scope)
@@ -183,22 +183,6 @@ module RobotLab
183
183
  self
184
184
  end
185
185
 
186
- # Convert to hash for JSON Schema
187
- #
188
- # @return [Hash] Map of tool names to their JSON schemas
189
- #
190
- def to_json_schema
191
- @tools.transform_values(&:to_json_schema)
192
- end
193
-
194
- # Convert to array of ruby_llm Tool classes
195
- #
196
- # @return [Array<Class>]
197
- #
198
- def to_ruby_llm_tools
199
- @tools.values.map(&:to_ruby_llm_tool)
200
- end
201
-
202
186
  # Converts the manifest to a hash representation.
203
187
  #
204
188
  # @return [Hash<String, Hash>] map of tool names to their hash representations
@@ -221,11 +205,11 @@ module RobotLab
221
205
  #
222
206
  def self.from_hash(hash)
223
207
  tools = hash.map do |name, config|
224
- Tool.new(
208
+ Tool.create(
225
209
  name: name,
226
210
  description: config[:description],
227
211
  parameters: config[:parameters],
228
- handler: config[:handler]
212
+ &config[:handler]
229
213
  )
230
214
  end
231
215
  new(tools)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RobotLab
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.4"
5
5
  end
data/lib/robot_lab.rb CHANGED
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "zeitwerk"
4
- require "json"
5
- require "securerandom"
6
- require "digest"
3
+ require 'zeitwerk'
4
+ require 'json'
5
+ require 'securerandom'
6
+ require 'digest'
7
7
 
8
8
  # Core dependencies
9
- require "ruby_llm"
10
- require "async"
9
+ # ActiveSupport delegation is required by ruby_llm (RubyLLM::Agent uses delegate)
10
+ # but not declared in ruby_llm's gemspec. Load it before ruby_llm.
11
+ require 'active_support/core_ext/module/delegation'
12
+ require 'ruby_llm'
13
+ require 'prompt_manager'
14
+ require 'async'
15
+ require 'typed_bus'
11
16
 
12
17
  # Define the module first so Zeitwerk can populate it
13
18
  #
@@ -29,10 +34,16 @@ require "async"
29
34
  # result = network.run(message: "Process this document")
30
35
  #
31
36
  # @example Configuration
32
- # RobotLab.configure do |config|
33
- # config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
34
- # config.template_path = "app/templates"
35
- # end
37
+ # # Via environment variables (ROBOT_LAB_* prefix)
38
+ # # ROBOT_LAB_DEFAULT_MODEL=gpt-4
39
+ # # ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
40
+ #
41
+ # # Or via config files (~/.config/robot_lab/config.yml or ./config/robot_lab.yml)
42
+ # # See lib/robot_lab/config/defaults.yml for all options
43
+ #
44
+ # # Access configuration values:
45
+ # RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
46
+ # RobotLab.config.ruby_llm.request_timeout #=> 120
36
47
  #
37
48
  module RobotLab
38
49
  end
@@ -43,16 +54,16 @@ loader.ignore("#{__dir__}/robot_lab/rails")
43
54
 
44
55
  # Custom inflections for classes that don't follow Zeitwerk naming conventions
45
56
  loader.inflector.inflect(
46
- "robot_lab" => "RobotLab",
47
- "robotic_model" => "RoboticModel",
48
- "mcp" => "MCP",
49
- "openai" => "OpenAI",
50
- "sse" => "SSE",
51
- "streamable_http" => "StreamableHTTP",
52
- "websocket" => "WebSocket"
57
+ 'robot_lab' => 'RobotLab',
58
+ 'robotic_model' => 'RoboticModel',
59
+ 'mcp' => 'MCP',
60
+ 'openai' => 'OpenAI',
61
+ 'sse' => 'SSE',
62
+ 'streamable_http' => 'StreamableHTTP',
63
+ 'websocket' => 'WebSocket'
53
64
  )
54
65
 
55
- # Note: adapters/ is NOT collapsed since files define RobotLab::Adapters::* classes
66
+ # NOTE: adapters/ is NOT collapsed since files define RobotLab::Adapters::* classes
56
67
 
57
68
  loader.setup
58
69
 
@@ -63,40 +74,51 @@ module RobotLab
63
74
  # Error classes are defined in lib/robot_lab/error.rb
64
75
 
65
76
  class << self
66
- # @!attribute [w] configuration
67
- # @return [Configuration] the configuration object
68
- attr_writer :configuration
69
-
70
- # Returns the current configuration object.
77
+ # Returns the Config object (MywayConfig-based).
78
+ #
79
+ # Configuration is automatically loaded from:
80
+ # - Bundled defaults (lib/robot_lab/config/defaults.yml)
81
+ # - Environment-specific overrides (development, test, production)
82
+ # - XDG config files (~/.config/robot_lab/config.yml)
83
+ # - Project config (./config/robot_lab.yml)
84
+ # - Environment variables (ROBOT_LAB_*)
71
85
  #
72
- # @return [Configuration] the configuration instance
73
- def configuration
74
- @configuration ||= Configuration.new
86
+ # @return [Config] the config instance
87
+ #
88
+ # @example
89
+ # RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
90
+ # RobotLab.config.ruby_llm.request_timeout #=> 120
91
+ # RobotLab.config.development? #=> true
92
+ def config
93
+ @config ||= Config.new.tap(&:after_load)
75
94
  end
76
95
 
77
- # Yields the configuration object for modification.
96
+
97
+ # Reload configuration from all sources.
78
98
  #
79
- # @yield [Configuration] the configuration object
80
- # @return [void]
99
+ # Clears the cached Config instance, forcing it to be
100
+ # reloaded on next access.
81
101
  #
82
- # @example
83
- # RobotLab.configure do |config|
84
- # config.anthropic_api_key = "sk-..."
85
- # end
86
- def configure
87
- yield(configuration)
102
+ # @return [Config] the new config instance
103
+ def reload_config!
104
+ @config = nil
105
+ config
88
106
  end
89
107
 
108
+
90
109
  # Factory method to create a new Robot instance.
91
110
  #
92
- # @param name [String] the unique identifier for the robot
93
- # @param template [Symbol, nil] the ERB template for the robot's prompt
111
+ # @param name [String, nil] the unique identifier for the robot (auto-generated if nil)
112
+ # @param template [Symbol, nil] the prompt_manager template for the robot's prompt
94
113
  # @param system_prompt [String, nil] inline system prompt (can be used alone or with template)
95
114
  # @param context [Hash] variables to pass to the template
96
115
  # @param enable_cache [Boolean] whether to enable semantic caching (default: true)
97
116
  # @param options [Hash] additional options passed to Robot.new
98
117
  # @return [Robot] a new Robot instance
99
- # @raise [ArgumentError] if neither template nor system_prompt is provided
118
+ #
119
+ # @example Bare robot (no template or prompt)
120
+ # robot = RobotLab.build
121
+ # robot.with_instructions("You are helpful.").run("Hello!")
100
122
  #
101
123
  # @example Robot with template
102
124
  # robot = RobotLab.build(
@@ -110,31 +132,19 @@ module RobotLab
110
132
  # name: "helper",
111
133
  # system_prompt: "You are a helpful assistant."
112
134
  # )
113
- #
114
- # @example Robot with both template and system prompt
115
- # robot = RobotLab.build(
116
- # name: "support",
117
- # template: :support_agent,
118
- # system_prompt: "Today's date is #{Date.today}."
119
- # )
120
- #
121
- # @example Robot with caching disabled
122
- # robot = RobotLab.build(
123
- # name: "simple",
124
- # system_prompt: "You are helpful.",
125
- # enable_cache: false
126
- # )
127
- def build(name:, template: nil, system_prompt: nil, context: {}, enable_cache: true, **options)
135
+ def build(name: "robot", template: nil, system_prompt: nil, context: {}, enable_cache: true, bus: nil, **options)
128
136
  Robot.new(
129
137
  name: name,
130
138
  template: template,
131
139
  system_prompt: system_prompt,
132
140
  context: context,
133
141
  enable_cache: enable_cache,
142
+ bus: bus,
134
143
  **options
135
144
  )
136
145
  end
137
146
 
147
+
138
148
  # Factory method to create a new Network of robots.
139
149
  #
140
150
  # @param name [String] the unique identifier for the network
@@ -166,6 +176,7 @@ module RobotLab
166
176
  Network.new(name: name, concurrency: concurrency, &block)
167
177
  end
168
178
 
179
+
169
180
  # Factory method to create a new Memory object.
170
181
  #
171
182
  # @param data [Hash] initial runtime data
@@ -190,6 +201,6 @@ end
190
201
 
191
202
  # Load Rails integration if Rails is defined
192
203
  if defined?(Rails::Engine)
193
- require "robot_lab/rails/engine"
194
- require "robot_lab/rails/railtie"
204
+ require 'robot_lab/rails/engine'
205
+ require 'robot_lab/rails/railtie'
195
206
  end