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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +64 -6
- data/Rakefile +2 -1
- data/docs/api/core/index.md +41 -46
- data/docs/api/core/memory.md +200 -154
- data/docs/api/core/network.md +13 -3
- data/docs/api/core/robot.md +38 -26
- data/docs/api/core/state.md +55 -73
- data/docs/api/index.md +7 -28
- 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/architecture/core-concepts.md +10 -15
- data/docs/concepts.md +5 -7
- data/docs/examples/index.md +2 -2
- data/docs/getting-started/configuration.md +80 -0
- data/docs/guides/building-robots.md +10 -9
- data/docs/guides/creating-networks.md +49 -0
- data/docs/guides/index.md +0 -5
- data/docs/guides/rails-integration.md +244 -162
- data/docs/guides/streaming.md +118 -138
- data/docs/index.md +0 -8
- data/examples/03_network.rb +10 -7
- data/examples/08_llm_config.rb +40 -11
- data/examples/09_chaining.rb +45 -6
- data/examples/11_network_introspection.rb +30 -7
- data/examples/12_message_bus.rb +1 -1
- data/examples/14_rusty_circuit/heckler.rb +14 -8
- data/examples/14_rusty_circuit/open_mic.rb +5 -3
- data/examples/14_rusty_circuit/scout.rb +14 -31
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +1 -1
- 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/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
- data/lib/robot_lab/memory.rb +8 -32
- 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 +56 -420
- data/lib/robot_lab/run_config.rb +184 -0
- data/lib/robot_lab/state_proxy.rb +2 -12
- data/lib/robot_lab/task.rb +8 -1
- data/lib/robot_lab/utils.rb +39 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +29 -8
- data/mkdocs.yml +0 -11
- metadata +15 -20
- 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 -275
- data/docs/api/history/config.md +0 -284
- data/docs/api/history/index.md +0 -128
- data/docs/api/history/thread-manager.md +0 -194
- data/docs/guides/history.md +0 -359
- 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 -160
- data/lib/robot_lab/adapters/registry.rb +0 -81
- 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
data/lib/robot_lab/robot.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'robot/template_rendering'
|
|
4
|
+
require_relative 'robot/mcp_management'
|
|
5
|
+
require_relative 'robot/bus_messaging'
|
|
6
|
+
|
|
3
7
|
module RobotLab
|
|
4
8
|
# LLM-powered robot built on RubyLLM::Agent
|
|
5
9
|
#
|
|
@@ -34,15 +38,9 @@ module RobotLab
|
|
|
34
38
|
# )
|
|
35
39
|
#
|
|
36
40
|
class Robot < RubyLLM::Agent
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
presence_penalty frequency_penalty stop
|
|
41
|
-
].freeze
|
|
42
|
-
|
|
43
|
-
# Front matter keys for robot identity and capabilities.
|
|
44
|
-
# Note: uses `robot_name` because PM::Metadata reserves `name` for the filename.
|
|
45
|
-
FRONT_MATTER_EXTRA_KEYS = %i[tools mcp robot_name description].freeze
|
|
41
|
+
include Robot::TemplateRendering
|
|
42
|
+
include Robot::MCPManagement
|
|
43
|
+
include Robot::BusMessaging
|
|
46
44
|
|
|
47
45
|
# @!attribute [r] name
|
|
48
46
|
# @return [String] the unique identifier for the robot
|
|
@@ -68,7 +66,7 @@ module RobotLab
|
|
|
68
66
|
|
|
69
67
|
attr_reader :name, :description, :template, :system_prompt,
|
|
70
68
|
:local_tools, :mcp_clients, :mcp_tools, :memory,
|
|
71
|
-
:bus, :outbox
|
|
69
|
+
:bus, :outbox, :config
|
|
72
70
|
|
|
73
71
|
# @!attribute [r] mcp_config
|
|
74
72
|
# @return [Symbol, Array] build-time MCP configuration (raw, unresolved)
|
|
@@ -99,6 +97,7 @@ module RobotLab
|
|
|
99
97
|
# @param presence_penalty [Float, nil] penalize based on presence
|
|
100
98
|
# @param frequency_penalty [Float, nil] penalize based on frequency
|
|
101
99
|
# @param stop [String, Array, nil] stop sequences
|
|
100
|
+
# @param config [RunConfig, nil] shared configuration (merged with explicit kwargs)
|
|
102
101
|
def initialize(
|
|
103
102
|
name:,
|
|
104
103
|
template: nil,
|
|
@@ -120,7 +119,8 @@ module RobotLab
|
|
|
120
119
|
max_tokens: nil,
|
|
121
120
|
presence_penalty: nil,
|
|
122
121
|
frequency_penalty: nil,
|
|
123
|
-
stop: nil
|
|
122
|
+
stop: nil,
|
|
123
|
+
config: nil
|
|
124
124
|
)
|
|
125
125
|
@name = name.to_s
|
|
126
126
|
@name_from_constructor = (name.to_s != "robot")
|
|
@@ -129,12 +129,32 @@ module RobotLab
|
|
|
129
129
|
@build_context = context
|
|
130
130
|
@description = description
|
|
131
131
|
@local_tools = Array(local_tools)
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
|
|
133
|
+
# Build RunConfig from explicit kwargs, merged on top of passed-in config.
|
|
134
|
+
# Explicit constructor kwargs always override the shared config.
|
|
135
|
+
explicit_fields = {
|
|
136
|
+
model: model, temperature: temperature, top_p: top_p, top_k: top_k,
|
|
137
|
+
max_tokens: max_tokens, presence_penalty: presence_penalty,
|
|
138
|
+
frequency_penalty: frequency_penalty, stop: stop,
|
|
139
|
+
on_tool_call: on_tool_call, on_tool_result: on_tool_result,
|
|
140
|
+
bus: bus, enable_cache: enable_cache
|
|
141
|
+
}.compact
|
|
142
|
+
|
|
143
|
+
# Only include mcp/tools if explicitly set (not the default :none sentinel)
|
|
144
|
+
resolved_mcp = mcp_servers.any? ? mcp_servers : mcp
|
|
145
|
+
explicit_fields[:mcp] = resolved_mcp unless ToolConfig.none_value?(resolved_mcp)
|
|
146
|
+
explicit_fields[:tools] = tools unless ToolConfig.none_value?(tools)
|
|
147
|
+
|
|
148
|
+
explicit_config = RunConfig.new(**explicit_fields)
|
|
149
|
+
@config = config ? config.merge(explicit_config) : explicit_config
|
|
150
|
+
|
|
151
|
+
# Extract values from effective config for backward compatibility
|
|
152
|
+
@on_tool_call = @config.on_tool_call
|
|
153
|
+
@on_tool_result = @config.on_tool_result
|
|
134
154
|
|
|
135
155
|
# Store raw config values for hierarchical resolution
|
|
136
|
-
@mcp_config =
|
|
137
|
-
@tools_config = tools
|
|
156
|
+
@mcp_config = @config.mcp || :none
|
|
157
|
+
@tools_config = @config.tools || :none
|
|
138
158
|
|
|
139
159
|
# MCP state
|
|
140
160
|
@mcp_clients = {}
|
|
@@ -142,19 +162,22 @@ module RobotLab
|
|
|
142
162
|
@mcp_initialized = false
|
|
143
163
|
|
|
144
164
|
# Bus state (optional inter-robot communication)
|
|
145
|
-
@bus = bus
|
|
165
|
+
@bus = @config.bus
|
|
146
166
|
@message_counter = 0
|
|
147
167
|
@outbox = {}
|
|
148
168
|
@message_handler = nil
|
|
169
|
+
@bus_processing = false
|
|
170
|
+
@bus_queue = []
|
|
149
171
|
|
|
150
172
|
# Inherent memory (used when standalone, not in a network)
|
|
151
|
-
|
|
173
|
+
cache_enabled = @config.key?(:enable_cache) ? @config.enable_cache : true
|
|
174
|
+
@memory = Memory.new(enable_cache: cache_enabled)
|
|
152
175
|
|
|
153
176
|
# Ensure config is loaded (triggers PM setup, RubyLLM config, etc.)
|
|
154
177
|
config = RobotLab.config
|
|
155
178
|
|
|
156
179
|
# Build chat kwargs for Agent's super
|
|
157
|
-
resolved_model = model || config.ruby_llm.model
|
|
180
|
+
resolved_model = @config.model || config.ruby_llm.model
|
|
158
181
|
chat_kwargs = { model: resolved_model }
|
|
159
182
|
|
|
160
183
|
# Create the persistent chat via Agent's initialize
|
|
@@ -165,14 +188,14 @@ module RobotLab
|
|
|
165
188
|
apply_template_to_chat(context) if @template
|
|
166
189
|
@chat.with_instructions(@system_prompt) if @system_prompt
|
|
167
190
|
|
|
168
|
-
# Constructor params override template front matter
|
|
169
|
-
apply_chat_option(:with_temperature, temperature)
|
|
170
|
-
apply_chat_option(:with_top_p, top_p)
|
|
171
|
-
apply_chat_option(:with_top_k, top_k)
|
|
172
|
-
apply_chat_option(:with_max_tokens, max_tokens)
|
|
173
|
-
apply_chat_option(:with_presence_penalty, presence_penalty)
|
|
174
|
-
apply_chat_option(:with_frequency_penalty, frequency_penalty)
|
|
175
|
-
apply_chat_option(:with_stop, stop)
|
|
191
|
+
# Constructor params override template front matter (use config values)
|
|
192
|
+
apply_chat_option(:with_temperature, @config.temperature)
|
|
193
|
+
apply_chat_option(:with_top_p, @config.top_p)
|
|
194
|
+
apply_chat_option(:with_top_k, @config.top_k)
|
|
195
|
+
apply_chat_option(:with_max_tokens, @config.max_tokens)
|
|
196
|
+
apply_chat_option(:with_presence_penalty, @config.presence_penalty)
|
|
197
|
+
apply_chat_option(:with_frequency_penalty, @config.frequency_penalty)
|
|
198
|
+
apply_chat_option(:with_stop, @config.stop)
|
|
176
199
|
|
|
177
200
|
# Apply callbacks
|
|
178
201
|
@chat.on_tool_call(&@on_tool_call) if @on_tool_call
|
|
@@ -206,18 +229,6 @@ module RobotLab
|
|
|
206
229
|
end
|
|
207
230
|
end
|
|
208
231
|
|
|
209
|
-
# Apply a prompt_manager template to the robot's chat
|
|
210
|
-
#
|
|
211
|
-
# @param template_id [Symbol, String] the template identifier
|
|
212
|
-
# @param context [Hash] variables to pass to the template
|
|
213
|
-
# @return [self]
|
|
214
|
-
def with_template(template_id, **context)
|
|
215
|
-
@template = template_id.to_sym
|
|
216
|
-
@build_context = context
|
|
217
|
-
apply_template_to_chat(context)
|
|
218
|
-
self
|
|
219
|
-
end
|
|
220
|
-
|
|
221
232
|
|
|
222
233
|
# Send a message and get a response, with Robot's extended capabilities
|
|
223
234
|
#
|
|
@@ -228,8 +239,8 @@ module RobotLab
|
|
|
228
239
|
# @param mcp [Symbol, Array, nil] runtime MCP override
|
|
229
240
|
# @param tools [Symbol, Array, nil] runtime tools override
|
|
230
241
|
# @return [RobotResult]
|
|
231
|
-
def run(message = nil, network: nil, network_memory: nil,
|
|
232
|
-
**kwargs)
|
|
242
|
+
def run(message = nil, network: nil, network_memory: nil, network_config: nil,
|
|
243
|
+
memory: nil, mcp: :none, tools: :none, **kwargs)
|
|
233
244
|
# Determine which memory to use
|
|
234
245
|
run_memory = resolve_active_memory(network: network, network_memory: network_memory)
|
|
235
246
|
|
|
@@ -247,8 +258,8 @@ module RobotLab
|
|
|
247
258
|
|
|
248
259
|
begin
|
|
249
260
|
# Resolve hierarchical MCP and tools configuration
|
|
250
|
-
resolved_mcp = resolve_mcp_hierarchy(mcp, network: network)
|
|
251
|
-
resolved_tools = resolve_tools_hierarchy(tools, network: network)
|
|
261
|
+
resolved_mcp = resolve_mcp_hierarchy(mcp, network: network, network_config: network_config)
|
|
262
|
+
resolved_tools = resolve_tools_hierarchy(tools, network: network, network_config: network_config)
|
|
252
263
|
|
|
253
264
|
# Initialize or update MCP clients based on resolved config
|
|
254
265
|
ensure_mcp_clients(resolved_mcp)
|
|
@@ -329,130 +340,6 @@ module RobotLab
|
|
|
329
340
|
end
|
|
330
341
|
|
|
331
342
|
|
|
332
|
-
# Send a message to another robot via the bus.
|
|
333
|
-
#
|
|
334
|
-
# @param to [String, Symbol] target robot's channel name
|
|
335
|
-
# @param content [String, Hash] message payload
|
|
336
|
-
# @return [RobotMessage] the sent message
|
|
337
|
-
# @raise [BusError] if no bus is configured
|
|
338
|
-
def send_message(to:, content:)
|
|
339
|
-
raise BusError, "No bus configured on robot '#{@name}'" unless @bus
|
|
340
|
-
|
|
341
|
-
@message_counter += 1
|
|
342
|
-
message = RobotMessage.build(id: @message_counter, from: @name, content: content)
|
|
343
|
-
@outbox[message.key] = { message: message, status: :sent, replies: [] }
|
|
344
|
-
publish_to_bus(to.to_sym, message)
|
|
345
|
-
message
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
# Send a reply to a specific message via the bus.
|
|
350
|
-
#
|
|
351
|
-
# @param to [String, Symbol] target robot's channel name
|
|
352
|
-
# @param content [String, Hash] reply payload
|
|
353
|
-
# @param in_reply_to [String] composite key of the message being replied to
|
|
354
|
-
# @return [RobotMessage] the reply message
|
|
355
|
-
# @raise [BusError] if no bus is configured
|
|
356
|
-
def send_reply(to:, content:, in_reply_to:)
|
|
357
|
-
raise BusError, "No bus configured on robot '#{@name}'" unless @bus
|
|
358
|
-
|
|
359
|
-
@message_counter += 1
|
|
360
|
-
reply = RobotMessage.build(id: @message_counter, from: @name, content: content, in_reply_to: in_reply_to)
|
|
361
|
-
publish_to_bus(to.to_sym, reply)
|
|
362
|
-
reply
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
# Convenience method to reply to a RobotMessage.
|
|
367
|
-
#
|
|
368
|
-
# @param message [RobotMessage] the message to reply to
|
|
369
|
-
# @param content [String, Hash] reply payload
|
|
370
|
-
# @return [RobotMessage] the reply message
|
|
371
|
-
def reply(message, content)
|
|
372
|
-
send_reply(to: message.from.to_sym, content: content, in_reply_to: message.key)
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
# Register a custom handler for incoming bus messages.
|
|
377
|
-
#
|
|
378
|
-
# Block arity controls delivery handling:
|
|
379
|
-
# - 1 argument `|message|`: auto-acks before calling, auto-nacks on exception
|
|
380
|
-
# - 2 arguments `|delivery, message|`: manual mode, you call ack!/nack!
|
|
381
|
-
#
|
|
382
|
-
# @yield [message] or [delivery, message]
|
|
383
|
-
# @return [self]
|
|
384
|
-
def on_message(&block)
|
|
385
|
-
@message_handler = block
|
|
386
|
-
self
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
# Spawn a new robot on a shared bus.
|
|
391
|
-
#
|
|
392
|
-
# Creates a new Robot instance that shares this robot's bus,
|
|
393
|
-
# allowing it to immediately send and receive messages with
|
|
394
|
-
# all other robots on the bus. If no bus exists yet, one is
|
|
395
|
-
# created automatically and the parent robot is connected to it.
|
|
396
|
-
#
|
|
397
|
-
# @param name [String] unique name for the new robot
|
|
398
|
-
# @param system_prompt [String, nil] inline system prompt
|
|
399
|
-
# @param template [Symbol, nil] prompt_manager template
|
|
400
|
-
# @param local_tools [Array] tools for the new robot
|
|
401
|
-
# @param options [Hash] additional options passed to RobotLab.build
|
|
402
|
-
# @return [Robot] the newly created robot
|
|
403
|
-
#
|
|
404
|
-
# @example Spawn from a bus-less robot (bus and name created automatically)
|
|
405
|
-
# bot = RobotLab.build
|
|
406
|
-
# bot2 = bot.spawn(system_prompt: "You are helpful.")
|
|
407
|
-
#
|
|
408
|
-
# @example Spawn a specialist from a message handler
|
|
409
|
-
# on_message do |message|
|
|
410
|
-
# specialist = spawn(
|
|
411
|
-
# name: "fact_checker",
|
|
412
|
-
# system_prompt: "You verify factual claims. Be concise."
|
|
413
|
-
# )
|
|
414
|
-
# specialist.send_message(to: name.to_sym, content: specialist.run(message.content).last_text_content)
|
|
415
|
-
# end
|
|
416
|
-
#
|
|
417
|
-
def spawn(name: "robot", system_prompt: nil, template: nil, local_tools: [], **options)
|
|
418
|
-
ensure_bus
|
|
419
|
-
|
|
420
|
-
RobotLab.build(
|
|
421
|
-
name: name,
|
|
422
|
-
system_prompt: system_prompt,
|
|
423
|
-
template: template,
|
|
424
|
-
local_tools: local_tools,
|
|
425
|
-
bus: @bus,
|
|
426
|
-
**options
|
|
427
|
-
)
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
# Connect this robot to a message bus.
|
|
432
|
-
#
|
|
433
|
-
# If a bus is provided, the robot joins it. If no bus is provided
|
|
434
|
-
# and the robot doesn't already have one, a new bus is created.
|
|
435
|
-
# No-op if the robot is already on the given bus.
|
|
436
|
-
#
|
|
437
|
-
# @param bus [TypedBus::MessageBus, nil] bus to join (creates one if nil)
|
|
438
|
-
# @return [self]
|
|
439
|
-
#
|
|
440
|
-
# @example Join an existing bus
|
|
441
|
-
# bot = RobotLab.build.with_bus(some_bus)
|
|
442
|
-
#
|
|
443
|
-
# @example Create a bus on demand
|
|
444
|
-
# bot = RobotLab.build.with_bus
|
|
445
|
-
#
|
|
446
|
-
def with_bus(bus = nil)
|
|
447
|
-
return self if bus && @bus == bus
|
|
448
|
-
|
|
449
|
-
teardown_bus_channel if @bus
|
|
450
|
-
@bus = bus || @bus || TypedBus::MessageBus.new
|
|
451
|
-
setup_bus_channel
|
|
452
|
-
self
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
|
|
456
343
|
# Disconnect all MCP clients and bus channel.
|
|
457
344
|
#
|
|
458
345
|
# @return [self]
|
|
@@ -478,6 +365,7 @@ module RobotLab
|
|
|
478
365
|
tools_config: @tools_config,
|
|
479
366
|
mcp_servers: @mcp_clients.keys,
|
|
480
367
|
model: model,
|
|
368
|
+
config: (@config.empty? ? nil : @config.to_json_hash),
|
|
481
369
|
bus: @bus ? true : nil
|
|
482
370
|
}.compact
|
|
483
371
|
end
|
|
@@ -490,107 +378,6 @@ module RobotLab
|
|
|
490
378
|
end
|
|
491
379
|
|
|
492
380
|
|
|
493
|
-
# Apply a prompt_manager template to the persistent chat.
|
|
494
|
-
# If required parameters are missing, applies front matter config but
|
|
495
|
-
# defers rendering until run time when all values are available.
|
|
496
|
-
def apply_template_to_chat(context)
|
|
497
|
-
parsed = PM.parse(@template)
|
|
498
|
-
|
|
499
|
-
# Extract extra config from front matter (name, description, tools, mcp)
|
|
500
|
-
apply_front_matter_extras(parsed.metadata)
|
|
501
|
-
|
|
502
|
-
# Extract and apply LLM config to the chat (model, temperature, etc.)
|
|
503
|
-
apply_front_matter_config(parsed.metadata)
|
|
504
|
-
|
|
505
|
-
# Resolve context (could be a Proc)
|
|
506
|
-
resolved_ctx = resolve_context(context, network: nil)
|
|
507
|
-
|
|
508
|
-
# Render the template body with context
|
|
509
|
-
begin
|
|
510
|
-
rendered = parsed.to_s(**resolved_ctx)
|
|
511
|
-
@chat.with_instructions(rendered)
|
|
512
|
-
rescue ArgumentError => e
|
|
513
|
-
raise unless e.message.start_with?("Missing required parameters:")
|
|
514
|
-
|
|
515
|
-
# Required parameters not yet available; template will be
|
|
516
|
-
# fully rendered at run time via rerender_template.
|
|
517
|
-
end
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
# Re-render the template with run-time context merged into build-time context.
|
|
522
|
-
# prompt_manager parameters may be required (null) and only available at run time.
|
|
523
|
-
def rerender_template(run_context)
|
|
524
|
-
merged = (@build_context || {}).merge(run_context)
|
|
525
|
-
parsed = PM.parse(@template)
|
|
526
|
-
resolved_ctx = resolve_context(merged, network: nil)
|
|
527
|
-
rendered = parsed.to_s(**resolved_ctx)
|
|
528
|
-
@chat.with_instructions(rendered)
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
# Extract whitelisted config from front matter and apply to chat
|
|
533
|
-
def apply_front_matter_config(metadata)
|
|
534
|
-
FRONT_MATTER_CONFIG_KEYS.each do |key|
|
|
535
|
-
value = metadata.respond_to?(key) ? metadata.send(key) : nil
|
|
536
|
-
next unless value
|
|
537
|
-
|
|
538
|
-
method = :"with_#{key}"
|
|
539
|
-
@chat.public_send(method, value) if @chat.respond_to?(method)
|
|
540
|
-
end
|
|
541
|
-
|
|
542
|
-
# Handle model specially (may need with_model)
|
|
543
|
-
return unless metadata.respond_to?(:model) && metadata.model
|
|
544
|
-
|
|
545
|
-
@chat.with_model(metadata.model)
|
|
546
|
-
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
# Extract identity and capability keys from front matter metadata.
|
|
551
|
-
# Constructor-provided values take precedence over frontmatter.
|
|
552
|
-
def apply_front_matter_extras(metadata)
|
|
553
|
-
if metadata.respond_to?(:robot_name) && metadata.robot_name && !@name_from_constructor
|
|
554
|
-
@name = metadata.robot_name.to_s
|
|
555
|
-
end
|
|
556
|
-
|
|
557
|
-
if metadata.respond_to?(:description) && metadata.description && @description.nil?
|
|
558
|
-
@description = metadata.description.to_s
|
|
559
|
-
end
|
|
560
|
-
|
|
561
|
-
if metadata.respond_to?(:tools) && metadata.tools.is_a?(Array) && @local_tools.empty?
|
|
562
|
-
@local_tools = resolve_frontmatter_tools(metadata.tools)
|
|
563
|
-
end
|
|
564
|
-
|
|
565
|
-
if metadata.respond_to?(:mcp) && metadata.mcp.is_a?(Array) && ToolConfig.none_value?(@mcp_config)
|
|
566
|
-
@mcp_config = metadata.mcp.map { |m| m.is_a?(Hash) ? m.transform_keys(&:to_sym) : m }
|
|
567
|
-
end
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
# Resolve string tool names from frontmatter to Ruby constants.
|
|
572
|
-
# Tool subclasses are instantiated; instances are used as-is.
|
|
573
|
-
# Unresolvable names are skipped with a warning.
|
|
574
|
-
def resolve_frontmatter_tools(tool_names)
|
|
575
|
-
tool_names.filter_map do |name|
|
|
576
|
-
case name
|
|
577
|
-
when String
|
|
578
|
-
begin
|
|
579
|
-
const = Object.const_get(name)
|
|
580
|
-
const.is_a?(Class) && const < RubyLLM::Tool ? const.new : const
|
|
581
|
-
rescue NameError
|
|
582
|
-
RobotLab.config.logger.warn("Robot '#{@name}': tool '#{name}' not found, skipping")
|
|
583
|
-
nil
|
|
584
|
-
end
|
|
585
|
-
when Class
|
|
586
|
-
name.new
|
|
587
|
-
else
|
|
588
|
-
name
|
|
589
|
-
end
|
|
590
|
-
end
|
|
591
|
-
end
|
|
592
|
-
|
|
593
|
-
|
|
594
381
|
# Determine which memory to use
|
|
595
382
|
def resolve_active_memory(network: nil, network_memory: nil)
|
|
596
383
|
network_memory || network&.memory || @memory
|
|
@@ -606,6 +393,7 @@ module RobotLab
|
|
|
606
393
|
tools = run_params.delete(:tools) || :none
|
|
607
394
|
memory = run_params.delete(:memory)
|
|
608
395
|
network_memory = run_params.delete(:network_memory)
|
|
396
|
+
network_config = run_params.delete(:network_config)
|
|
609
397
|
|
|
610
398
|
# Build base context from remaining run params
|
|
611
399
|
base = run_params.dup
|
|
@@ -627,20 +415,12 @@ module RobotLab
|
|
|
627
415
|
merged[:tools] = tools
|
|
628
416
|
merged[:memory] = memory if memory
|
|
629
417
|
merged[:network_memory] = network_memory if network_memory
|
|
418
|
+
merged[:network_config] = network_config if network_config
|
|
630
419
|
|
|
631
420
|
merged
|
|
632
421
|
end
|
|
633
422
|
|
|
634
423
|
|
|
635
|
-
def resolve_context(context, network:)
|
|
636
|
-
case context
|
|
637
|
-
when Proc then context.call(network: network)
|
|
638
|
-
when Hash then context
|
|
639
|
-
else {}
|
|
640
|
-
end
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
|
|
644
424
|
def build_result(response, _memory)
|
|
645
425
|
output = if response.respond_to?(:content) && response.content
|
|
646
426
|
[TextMessage.new(role: 'assistant', content: response.content)]
|
|
@@ -675,81 +455,6 @@ module RobotLab
|
|
|
675
455
|
end
|
|
676
456
|
|
|
677
457
|
|
|
678
|
-
# Resolve MCP hierarchy: runtime -> robot build -> network -> config
|
|
679
|
-
def resolve_mcp_hierarchy(runtime_value, network:)
|
|
680
|
-
parent_value = network&.network&.mcp || RobotLab.config.mcp
|
|
681
|
-
build_resolved = ToolConfig.resolve_mcp(@mcp_config, parent_value: parent_value)
|
|
682
|
-
ToolConfig.resolve_mcp(runtime_value, parent_value: build_resolved)
|
|
683
|
-
end
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
# Resolve tools hierarchy: runtime -> robot build -> network -> config
|
|
687
|
-
def resolve_tools_hierarchy(runtime_value, network:)
|
|
688
|
-
parent_value = network&.network&.tools || RobotLab.config.tools
|
|
689
|
-
build_resolved = ToolConfig.resolve_tools(@tools_config, parent_value: parent_value)
|
|
690
|
-
ToolConfig.resolve_tools(runtime_value, parent_value: build_resolved)
|
|
691
|
-
end
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
# Ensure MCP clients are initialized for the given server configs
|
|
695
|
-
def ensure_mcp_clients(mcp_servers)
|
|
696
|
-
return if mcp_servers.empty?
|
|
697
|
-
|
|
698
|
-
needed_servers = mcp_servers.map { |s| s.is_a?(Hash) ? s[:name] : s.to_s }.compact
|
|
699
|
-
return if @mcp_initialized && (@mcp_clients.keys.sort == needed_servers.sort)
|
|
700
|
-
|
|
701
|
-
disconnect if @mcp_initialized
|
|
702
|
-
|
|
703
|
-
@mcp_clients = {}
|
|
704
|
-
@mcp_tools = []
|
|
705
|
-
|
|
706
|
-
mcp_servers.each do |server_config|
|
|
707
|
-
init_mcp_client(server_config)
|
|
708
|
-
end
|
|
709
|
-
|
|
710
|
-
@mcp_initialized = true
|
|
711
|
-
end
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
def init_mcp_client(server_config)
|
|
715
|
-
client = MCP::Client.new(server_config)
|
|
716
|
-
client.connect
|
|
717
|
-
|
|
718
|
-
if client.connected?
|
|
719
|
-
server_name = client.server.name
|
|
720
|
-
@mcp_clients[server_name] = client
|
|
721
|
-
discover_mcp_tools(client, server_name)
|
|
722
|
-
else
|
|
723
|
-
RobotLab.config.logger.warn(
|
|
724
|
-
"Robot '#{@name}' failed to connect to MCP server: #{server_config[:name] || server_config}"
|
|
725
|
-
)
|
|
726
|
-
end
|
|
727
|
-
end
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
def discover_mcp_tools(client, server_name)
|
|
731
|
-
tools = client.list_tools
|
|
732
|
-
|
|
733
|
-
tools.each do |tool_def|
|
|
734
|
-
tool_name = tool_def[:name]
|
|
735
|
-
mcp_client = client
|
|
736
|
-
|
|
737
|
-
tool = Tool.create(
|
|
738
|
-
name: tool_name,
|
|
739
|
-
description: tool_def[:description],
|
|
740
|
-
parameters: tool_def[:inputSchema],
|
|
741
|
-
mcp: server_name
|
|
742
|
-
) { |args| mcp_client.call_tool(tool_name, args) }
|
|
743
|
-
|
|
744
|
-
@mcp_tools << tool
|
|
745
|
-
end
|
|
746
|
-
|
|
747
|
-
RobotLab.config.logger.info(
|
|
748
|
-
"Robot '#{@name}' discovered #{tools.size} tools from MCP server '#{server_name}'"
|
|
749
|
-
)
|
|
750
|
-
end
|
|
751
|
-
|
|
752
|
-
|
|
753
458
|
def all_tools
|
|
754
459
|
@local_tools + @mcp_tools
|
|
755
460
|
end
|
|
@@ -761,74 +466,5 @@ module RobotLab
|
|
|
761
466
|
|
|
762
467
|
ToolConfig.filter_tools(available, allowed_names: allowed_names)
|
|
763
468
|
end
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
# Create a bus if one doesn't exist and connect this robot to it
|
|
767
|
-
def ensure_bus
|
|
768
|
-
with_bus unless @bus
|
|
769
|
-
end
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
# Create a typed channel on the bus and subscribe to it
|
|
773
|
-
def setup_bus_channel
|
|
774
|
-
channel_name = @name.to_sym
|
|
775
|
-
@bus.add_channel(channel_name, type: RobotMessage) unless @bus.channel?(channel_name)
|
|
776
|
-
@bus_subscriber_id = @bus.subscribe(channel_name) { |delivery| handle_incoming_delivery(delivery) }
|
|
777
|
-
end
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
# Unsubscribe from the bus channel
|
|
781
|
-
def teardown_bus_channel
|
|
782
|
-
channel_name = @name.to_sym
|
|
783
|
-
@bus.unsubscribe(channel_name, @bus_subscriber_id) if @bus_subscriber_id
|
|
784
|
-
@bus_subscriber_id = nil
|
|
785
|
-
end
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
# Dispatch incoming bus delivery to handler.
|
|
789
|
-
# Auto-ack when the handler takes 1 arg (message only);
|
|
790
|
-
# manual ack/nack when the handler takes 2 args (delivery, message).
|
|
791
|
-
def handle_incoming_delivery(delivery)
|
|
792
|
-
message = delivery.message
|
|
793
|
-
|
|
794
|
-
# Correlate replies with outbox entries
|
|
795
|
-
if message.reply? && @outbox.key?(message.in_reply_to)
|
|
796
|
-
entry = @outbox[message.in_reply_to]
|
|
797
|
-
entry[:status] = :replied
|
|
798
|
-
entry[:replies] << message
|
|
799
|
-
end
|
|
800
|
-
|
|
801
|
-
if @message_handler
|
|
802
|
-
if @message_handler.arity == 1
|
|
803
|
-
delivery.ack!
|
|
804
|
-
@message_handler.call(message)
|
|
805
|
-
else
|
|
806
|
-
@message_handler.call(delivery, message)
|
|
807
|
-
end
|
|
808
|
-
else
|
|
809
|
-
handle_message_via_llm(delivery, message)
|
|
810
|
-
end
|
|
811
|
-
rescue => e
|
|
812
|
-
delivery.nack! if delivery.pending?
|
|
813
|
-
raise BusError, "Error handling bus message on robot '#{@name}': #{e.message}"
|
|
814
|
-
end
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
# Default handler: interpret message via LLM and reply
|
|
818
|
-
def handle_message_via_llm(delivery, message)
|
|
819
|
-
delivery.ack!
|
|
820
|
-
result = run(message.content.to_s)
|
|
821
|
-
send_reply(to: message.from.to_sym, content: result.last_text_content, in_reply_to: message.key)
|
|
822
|
-
end
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
# Publish a RobotMessage to a bus channel
|
|
826
|
-
def publish_to_bus(channel_name, message)
|
|
827
|
-
if defined?(Async::Task) && Async::Task.current?
|
|
828
|
-
@bus.publish(channel_name, message)
|
|
829
|
-
else
|
|
830
|
-
Async { @bus.publish(channel_name, message) }
|
|
831
|
-
end
|
|
832
|
-
end
|
|
833
469
|
end
|
|
834
470
|
end
|