robot_lab 0.0.1 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +9 -9
- data/.irbrc +6 -0
- data/CHANGELOG.md +140 -0
- data/README.md +263 -48
- data/Rakefile +71 -1
- data/docs/api/core/index.md +53 -46
- data/docs/api/core/memory.md +200 -154
- data/docs/api/core/network.md +13 -3
- data/docs/api/core/robot.md +490 -130
- data/docs/api/core/state.md +55 -73
- data/docs/api/core/tool.md +205 -209
- data/docs/api/index.md +7 -28
- data/docs/api/mcp/client.md +119 -48
- data/docs/api/mcp/index.md +75 -60
- data/docs/api/mcp/server.md +120 -136
- data/docs/api/mcp/transports.md +172 -184
- data/docs/api/messages/index.md +35 -20
- data/docs/api/messages/text-message.md +67 -21
- data/docs/api/messages/tool-call-message.md +80 -41
- data/docs/api/messages/tool-result-message.md +119 -50
- data/docs/api/messages/user-message.md +48 -24
- data/docs/api/streaming/context.md +157 -74
- data/docs/api/streaming/events.md +114 -166
- data/docs/api/streaming/index.md +74 -72
- data/docs/architecture/core-concepts.md +360 -116
- data/docs/architecture/index.md +97 -59
- data/docs/architecture/message-flow.md +138 -129
- data/docs/architecture/network-orchestration.md +197 -50
- data/docs/architecture/robot-execution.md +199 -146
- data/docs/architecture/state-management.md +255 -187
- data/docs/concepts.md +311 -49
- data/docs/examples/basic-chat.md +89 -77
- data/docs/examples/index.md +222 -47
- data/docs/examples/mcp-server.md +207 -203
- data/docs/examples/multi-robot-network.md +129 -35
- data/docs/examples/rails-application.md +159 -160
- data/docs/examples/tool-usage.md +295 -204
- data/docs/getting-started/configuration.md +347 -154
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/installation.md +22 -13
- data/docs/getting-started/quick-start.md +166 -121
- data/docs/guides/building-robots.md +418 -212
- data/docs/guides/creating-networks.md +143 -24
- data/docs/guides/index.md +0 -5
- data/docs/guides/mcp-integration.md +152 -113
- data/docs/guides/memory.md +220 -164
- data/docs/guides/rails-integration.md +244 -162
- data/docs/guides/streaming.md +137 -187
- data/docs/guides/using-tools.md +259 -212
- data/docs/index.md +46 -41
- data/examples/01_simple_robot.rb +6 -9
- data/examples/02_tools.rb +6 -9
- data/examples/03_network.rb +19 -17
- data/examples/04_mcp.rb +5 -8
- data/examples/05_streaming.rb +5 -8
- data/examples/06_prompt_templates.rb +42 -37
- data/examples/07_network_memory.rb +13 -14
- data/examples/08_llm_config.rb +169 -0
- data/examples/09_chaining.rb +262 -0
- data/examples/10_memory.rb +331 -0
- data/examples/11_network_introspection.rb +253 -0
- data/examples/12_message_bus.rb +74 -0
- data/examples/13_spawn.rb +90 -0
- data/examples/14_rusty_circuit/comic.rb +143 -0
- data/examples/14_rusty_circuit/display.rb +203 -0
- data/examples/14_rusty_circuit/heckler.rb +63 -0
- data/examples/14_rusty_circuit/open_mic.rb +123 -0
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
- data/examples/14_rusty_circuit/scout.rb +156 -0
- data/examples/14_rusty_circuit/scout_notes.md +89 -0
- data/examples/14_rusty_circuit/show.log +234 -0
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
- data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
- data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
- data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
- data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
- data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
- data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
- data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
- data/examples/15_memory_network_and_bus/output/memory.json +13 -0
- data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
- data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
- data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
- data/examples/16_writers_room/display.rb +158 -0
- data/examples/16_writers_room/output/.gitignore +2 -0
- data/examples/16_writers_room/output/opus_001.md +263 -0
- data/examples/16_writers_room/output/opus_001_notes.log +470 -0
- data/examples/16_writers_room/prompts/writer.md +37 -0
- data/examples/16_writers_room/room.rb +150 -0
- data/examples/16_writers_room/tools.rb +162 -0
- data/examples/16_writers_room/writer.rb +121 -0
- data/examples/16_writers_room/writers_room.rb +162 -0
- data/examples/README.md +197 -0
- data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
- data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
- data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
- data/examples/prompts/comedian.md +6 -0
- data/examples/prompts/comedy_critic.md +10 -0
- data/examples/prompts/configurable.md +9 -0
- data/examples/prompts/dispatcher.md +12 -0
- data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
- data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
- data/examples/prompts/frontmatter_mcp_test.md +9 -0
- data/examples/prompts/frontmatter_named_test.md +5 -0
- data/examples/prompts/frontmatter_tools_test.md +6 -0
- data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
- data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
- data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
- data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
- data/examples/prompts/llm_config_demo.md +20 -0
- data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
- data/examples/prompts/os_advocate.md +13 -0
- data/examples/prompts/os_chief.md +13 -0
- data/examples/prompts/os_editor.md +13 -0
- data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
- data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
- data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
- data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
- data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
- data/lib/robot_lab/ask_user.rb +75 -0
- data/lib/robot_lab/config/defaults.yml +121 -0
- data/lib/robot_lab/config.rb +183 -0
- data/lib/robot_lab/error.rb +6 -0
- data/lib/robot_lab/mcp/client.rb +1 -1
- data/lib/robot_lab/memory.rb +10 -34
- data/lib/robot_lab/network.rb +13 -20
- data/lib/robot_lab/robot/bus_messaging.rb +239 -0
- data/lib/robot_lab/robot/mcp_management.rb +88 -0
- data/lib/robot_lab/robot/template_rendering.rb +130 -0
- data/lib/robot_lab/robot.rb +240 -330
- data/lib/robot_lab/robot_message.rb +44 -0
- data/lib/robot_lab/robot_result.rb +1 -0
- data/lib/robot_lab/run_config.rb +184 -0
- data/lib/robot_lab/state_proxy.rb +2 -12
- data/lib/robot_lab/streaming/context.rb +1 -1
- data/lib/robot_lab/task.rb +8 -1
- data/lib/robot_lab/tool.rb +108 -172
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +2 -18
- data/lib/robot_lab/utils.rb +39 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +89 -57
- data/mkdocs.yml +0 -11
- metadata +121 -135
- data/docs/api/adapters/anthropic.md +0 -121
- data/docs/api/adapters/gemini.md +0 -133
- data/docs/api/adapters/index.md +0 -104
- data/docs/api/adapters/openai.md +0 -134
- data/docs/api/history/active-record-adapter.md +0 -195
- data/docs/api/history/config.md +0 -191
- data/docs/api/history/index.md +0 -132
- data/docs/api/history/thread-manager.md +0 -144
- data/docs/guides/history.md +0 -359
- data/examples/prompts/assistant/user.txt.erb +0 -1
- data/examples/prompts/billing/user.txt.erb +0 -1
- data/examples/prompts/classifier/user.txt.erb +0 -1
- data/examples/prompts/entity_extractor/user.txt.erb +0 -3
- data/examples/prompts/escalation/user.txt.erb +0 -34
- data/examples/prompts/general/user.txt.erb +0 -1
- data/examples/prompts/github_assistant/user.txt.erb +0 -1
- data/examples/prompts/helper/user.txt.erb +0 -1
- data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
- data/examples/prompts/order_support/user.txt.erb +0 -22
- data/examples/prompts/product_support/user.txt.erb +0 -32
- data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
- data/examples/prompts/synthesizer/user.txt.erb +0 -15
- data/examples/prompts/technical/user.txt.erb +0 -1
- data/examples/prompts/triage/user.txt.erb +0 -17
- data/lib/robot_lab/adapters/anthropic.rb +0 -163
- data/lib/robot_lab/adapters/base.rb +0 -85
- data/lib/robot_lab/adapters/gemini.rb +0 -193
- data/lib/robot_lab/adapters/openai.rb +0 -159
- data/lib/robot_lab/adapters/registry.rb +0 -81
- data/lib/robot_lab/configuration.rb +0 -143
- data/lib/robot_lab/errors.rb +0 -70
- data/lib/robot_lab/history/active_record_adapter.rb +0 -146
- data/lib/robot_lab/history/config.rb +0 -115
- data/lib/robot_lab/history/thread_manager.rb +0 -93
- data/lib/robot_lab/robotic_model.rb +0 -324
|
@@ -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
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
# Shared configuration object for LLM, tools, callbacks, and infrastructure.
|
|
5
|
+
#
|
|
6
|
+
# RunConfig provides a unified way to express operational defaults that flow
|
|
7
|
+
# through the configuration hierarchy:
|
|
8
|
+
#
|
|
9
|
+
# RobotLab.config -> Network -> Robot -> Template front matter -> Task -> Runtime
|
|
10
|
+
#
|
|
11
|
+
# Only explicitly set values are stored. Merge semantics: the more-specific
|
|
12
|
+
# config's non-nil values win over the less-specific config.
|
|
13
|
+
#
|
|
14
|
+
# @example Keyword construction
|
|
15
|
+
# config = RunConfig.new(model: "claude-sonnet-4", temperature: 0.7)
|
|
16
|
+
#
|
|
17
|
+
# @example Block DSL
|
|
18
|
+
# config = RunConfig.new do |c|
|
|
19
|
+
# c.model "claude-sonnet-4"
|
|
20
|
+
# c.temperature 0.7
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example Merge (more-specific wins)
|
|
24
|
+
# network_config = RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
|
|
25
|
+
# robot_config = RunConfig.new(temperature: 0.9)
|
|
26
|
+
# effective = network_config.merge(robot_config)
|
|
27
|
+
# effective.temperature #=> 0.9
|
|
28
|
+
# effective.model #=> "claude-sonnet-4"
|
|
29
|
+
#
|
|
30
|
+
class RunConfig
|
|
31
|
+
# LLM configuration fields (applied to chat via with_* methods)
|
|
32
|
+
LLM_FIELDS = %i[
|
|
33
|
+
model temperature top_p top_k max_tokens
|
|
34
|
+
presence_penalty frequency_penalty stop
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
# Tool-related fields
|
|
38
|
+
TOOL_FIELDS = %i[mcp tools].freeze
|
|
39
|
+
|
|
40
|
+
# Callback fields (Procs)
|
|
41
|
+
CALLBACK_FIELDS = %i[on_tool_call on_tool_result].freeze
|
|
42
|
+
|
|
43
|
+
# Infrastructure fields
|
|
44
|
+
INFRA_FIELDS = %i[bus enable_cache].freeze
|
|
45
|
+
|
|
46
|
+
# All recognized fields
|
|
47
|
+
FIELDS = (LLM_FIELDS + TOOL_FIELDS + CALLBACK_FIELDS + INFRA_FIELDS).freeze
|
|
48
|
+
|
|
49
|
+
# Fields that cannot be serialized to JSON (Procs, IO objects, etc.)
|
|
50
|
+
NON_SERIALIZABLE_FIELDS = (CALLBACK_FIELDS + %i[bus]).freeze
|
|
51
|
+
|
|
52
|
+
# Creates a new RunConfig.
|
|
53
|
+
#
|
|
54
|
+
# @param kwargs [Hash] field values to set
|
|
55
|
+
# @yield [self] optional block for DSL-style configuration
|
|
56
|
+
def initialize(**kwargs)
|
|
57
|
+
@fields = {}
|
|
58
|
+
|
|
59
|
+
kwargs.each do |key, value|
|
|
60
|
+
set(key, value)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
yield self if block_given?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Define getter/setter DSL methods for each field.
|
|
67
|
+
#
|
|
68
|
+
# With no arguments: returns the current value (getter).
|
|
69
|
+
# With one argument: sets the value and returns self (chainable setter).
|
|
70
|
+
FIELDS.each do |field|
|
|
71
|
+
define_method(field) do |value = :__unset__|
|
|
72
|
+
if value == :__unset__
|
|
73
|
+
@fields[field]
|
|
74
|
+
else
|
|
75
|
+
set(field, value)
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns a duplicate of the internal fields hash.
|
|
82
|
+
#
|
|
83
|
+
# @return [Hash] only the fields that have been explicitly set
|
|
84
|
+
def to_h
|
|
85
|
+
@fields.dup
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Returns a JSON-safe hash (skips Procs, IO, and other non-serializable values).
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash]
|
|
92
|
+
def to_json_hash
|
|
93
|
+
@fields.reject { |k, _| NON_SERIALIZABLE_FIELDS.include?(k) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# Merges another RunConfig (or Hash) on top of this one.
|
|
98
|
+
# The other's non-nil values win. Returns a new RunConfig.
|
|
99
|
+
#
|
|
100
|
+
# @param other [RunConfig, Hash] the more-specific configuration
|
|
101
|
+
# @return [RunConfig] a new merged RunConfig
|
|
102
|
+
def merge(other)
|
|
103
|
+
other_hash = other.is_a?(RunConfig) ? other.to_h : other
|
|
104
|
+
merged = @fields.merge(other_hash) { |_k, old_v, new_v| new_v.nil? ? old_v : new_v }
|
|
105
|
+
self.class.new(**merged)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Applies LLM fields to a chat object via its with_* methods.
|
|
110
|
+
#
|
|
111
|
+
# @param chat [Object] a RubyLLM::Chat (or similar) that responds to with_model, with_temperature, etc.
|
|
112
|
+
def apply_to(chat)
|
|
113
|
+
LLM_FIELDS.each do |field|
|
|
114
|
+
value = @fields[field]
|
|
115
|
+
next unless value
|
|
116
|
+
|
|
117
|
+
method = :"with_#{field}"
|
|
118
|
+
chat.public_send(method, value) if chat.respond_to?(method)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# Build a RunConfig from prompt_manager front matter metadata.
|
|
124
|
+
#
|
|
125
|
+
# @param metadata [Object] a PM::Metadata object (responds to field names)
|
|
126
|
+
# @return [RunConfig]
|
|
127
|
+
def self.from_front_matter(metadata)
|
|
128
|
+
fields = {}
|
|
129
|
+
|
|
130
|
+
LLM_FIELDS.each do |key|
|
|
131
|
+
value = metadata.respond_to?(key) ? metadata.send(key) : nil
|
|
132
|
+
fields[key] = value if value
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Extract tool-related fields
|
|
136
|
+
%i[mcp tools].each do |key|
|
|
137
|
+
value = metadata.respond_to?(key) ? metadata.send(key) : nil
|
|
138
|
+
fields[key] = value if value
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
new(**fields)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# @return [Boolean] true if no fields have been set
|
|
146
|
+
def empty?
|
|
147
|
+
@fields.empty?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# @param field [Symbol] the field name
|
|
152
|
+
# @return [Boolean] true if the field has been explicitly set
|
|
153
|
+
def key?(field)
|
|
154
|
+
@fields.key?(field)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# @param other [RunConfig] the other RunConfig to compare
|
|
159
|
+
# @return [Boolean]
|
|
160
|
+
def ==(other)
|
|
161
|
+
other.is_a?(RunConfig) && to_h == other.to_h
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# @return [String]
|
|
166
|
+
def inspect
|
|
167
|
+
"#<#{self.class} #{@fields.inspect}>"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
# Validates and stores a field value. Nil removes the key.
|
|
173
|
+
def set(field, value)
|
|
174
|
+
field = field.to_sym
|
|
175
|
+
raise ArgumentError, "Unknown RunConfig field: #{field}" unless FIELDS.include?(field)
|
|
176
|
+
|
|
177
|
+
if value.nil?
|
|
178
|
+
@fields.delete(field)
|
|
179
|
+
else
|
|
180
|
+
@fields[field] = value
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -16,6 +16,8 @@ module RobotLab
|
|
|
16
16
|
# proxy.to_h # => { count: 1, name: "test" }
|
|
17
17
|
#
|
|
18
18
|
class StateProxy
|
|
19
|
+
include Utils
|
|
20
|
+
|
|
19
21
|
# Creates a new StateProxy.
|
|
20
22
|
#
|
|
21
23
|
# @param data [Hash] the initial data
|
|
@@ -172,17 +174,5 @@ module RobotLab
|
|
|
172
174
|
"#<RobotLab::StateProxy #{@data.inspect}>"
|
|
173
175
|
end
|
|
174
176
|
|
|
175
|
-
private
|
|
176
|
-
|
|
177
|
-
def deep_dup(obj)
|
|
178
|
-
case obj
|
|
179
|
-
when Hash
|
|
180
|
-
obj.transform_values { |v| deep_dup(v) }
|
|
181
|
-
when Array
|
|
182
|
-
obj.map { |v| deep_dup(v) }
|
|
183
|
-
else
|
|
184
|
-
obj.dup rescue obj
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
177
|
end
|
|
188
178
|
end
|
data/lib/robot_lab/task.rb
CHANGED
|
@@ -39,13 +39,14 @@ module RobotLab
|
|
|
39
39
|
# @param tools [Symbol, Array] tools config (:none, :inherit, or array)
|
|
40
40
|
# @param memory [Memory, Hash, nil] task-specific memory
|
|
41
41
|
#
|
|
42
|
-
def initialize(name:, robot:, context: {}, mcp: :none, tools: :none, memory: nil)
|
|
42
|
+
def initialize(name:, robot:, context: {}, mcp: :none, tools: :none, memory: nil, config: nil)
|
|
43
43
|
@name = name.to_sym
|
|
44
44
|
@robot = robot
|
|
45
45
|
@context = context
|
|
46
46
|
@mcp = mcp
|
|
47
47
|
@tools = tools
|
|
48
48
|
@memory = memory
|
|
49
|
+
@config = config
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
# SimpleFlow step interface
|
|
@@ -68,6 +69,12 @@ module RobotLab
|
|
|
68
69
|
run_params[:tools] = @tools unless @tools == :none
|
|
69
70
|
run_params[:memory] = @memory if @memory
|
|
70
71
|
|
|
72
|
+
# Merge task's config on top of network's config
|
|
73
|
+
if @config
|
|
74
|
+
network_rc = run_params[:network_config]
|
|
75
|
+
run_params[:network_config] = network_rc ? network_rc.merge(@config) : @config
|
|
76
|
+
end
|
|
77
|
+
|
|
71
78
|
# Create enhanced result with merged params
|
|
72
79
|
enhanced_result = result.with_context(:run_params, run_params)
|
|
73
80
|
|
data/lib/robot_lab/tool.rb
CHANGED
|
@@ -1,222 +1,158 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RobotLab
|
|
4
|
-
#
|
|
4
|
+
# A tool that robots can use, built on RubyLLM::Tool.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
# They have a name, description, parameter schema, and handler.
|
|
6
|
+
# Provides two patterns for defining tools:
|
|
8
7
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# description
|
|
13
|
-
#
|
|
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
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
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
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# @!attribute [
|
|
38
|
-
# @return [
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
#
|
|
47
|
+
# Override name to support explicit names for dynamic/MCP tools.
|
|
71
48
|
#
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
#
|
|
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
|
-
|
|
83
|
-
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def mcp?
|
|
58
|
+
!@mcp.nil?
|
|
59
|
+
end
|
|
84
60
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 =
|
|
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
|
-
#
|
|
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]
|
|
129
|
+
# @return [Hash]
|
|
153
130
|
def to_h
|
|
154
131
|
{
|
|
155
132
|
name: name,
|
|
156
133
|
description: description,
|
|
157
|
-
|
|
158
|
-
mcp: mcp,
|
|
159
|
-
strict: strict
|
|
134
|
+
mcp: mcp
|
|
160
135
|
}.compact
|
|
161
136
|
end
|
|
162
137
|
|
|
163
|
-
#
|
|
138
|
+
# JSON representation.
|
|
164
139
|
#
|
|
165
140
|
# @param args [Array] arguments passed to to_json
|
|
166
|
-
# @return [String]
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
208
|
+
Tool.create(
|
|
225
209
|
name: name,
|
|
226
210
|
description: config[:description],
|
|
227
211
|
parameters: config[:parameters],
|
|
228
|
-
|
|
212
|
+
&config[:handler]
|
|
229
213
|
)
|
|
230
214
|
end
|
|
231
215
|
new(tools)
|