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
data/lib/robot_lab/robot.rb
CHANGED
|
@@ -1,76 +1,55 @@
|
|
|
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
|
-
# LLM-powered robot
|
|
8
|
+
# LLM-powered robot built on RubyLLM::Agent
|
|
5
9
|
#
|
|
6
|
-
# Robot is a
|
|
7
|
-
# - Template-based prompts via
|
|
8
|
-
# -
|
|
9
|
-
# -
|
|
10
|
-
# -
|
|
11
|
-
# - Hierarchical MCP and tools configuration
|
|
10
|
+
# Robot is a subclass of RubyLLM::Agent that adds:
|
|
11
|
+
# - Template-based prompts via prompt_manager
|
|
12
|
+
# - Shared memory (standalone or network)
|
|
13
|
+
# - Tool integration with hierarchical MCP configuration
|
|
14
|
+
# - SimpleFlow pipeline integration
|
|
12
15
|
#
|
|
13
16
|
# == Memory Behavior
|
|
14
17
|
#
|
|
15
|
-
# Robots have two memory contexts depending on how they're used:
|
|
16
|
-
#
|
|
17
18
|
# *Standalone*: Robot uses its own inherent memory (`robot.memory`).
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# *In a Network*: Robot uses the network's shared memory (`network.memory`).
|
|
21
|
-
# The robot's inherent memory is ignored. Use `network.reset_memory` to clear it.
|
|
22
|
-
#
|
|
23
|
-
# This allows the same robot instance to work both standalone and as part
|
|
24
|
-
# of a network, with appropriate memory isolation in each context.
|
|
19
|
+
# *In a Network*: Robot uses the network's shared memory.
|
|
25
20
|
#
|
|
26
21
|
# @example Simple robot with template
|
|
27
|
-
# robot = Robot.new(
|
|
28
|
-
#
|
|
29
|
-
# template: :helper,
|
|
30
|
-
# context: { company_name: "Acme Corp" }
|
|
31
|
-
# )
|
|
32
|
-
# result = robot.run(message: "Hello!", user_name: "Alice")
|
|
22
|
+
# robot = Robot.new(name: "helper", template: :helper)
|
|
23
|
+
# result = robot.run("Hello!")
|
|
33
24
|
#
|
|
34
|
-
# @example Robot with inline system prompt
|
|
35
|
-
# robot = Robot.new(
|
|
36
|
-
#
|
|
37
|
-
# system_prompt: "You are a helpful assistant. Be concise."
|
|
38
|
-
# )
|
|
25
|
+
# @example Robot with inline system prompt
|
|
26
|
+
# robot = Robot.new(name: "bot", system_prompt: "You are helpful.")
|
|
27
|
+
# result = robot.run("What is 2+2?")
|
|
39
28
|
#
|
|
40
|
-
# @example
|
|
41
|
-
# robot = Robot.new(
|
|
42
|
-
#
|
|
43
|
-
# template: :support_agent,
|
|
44
|
-
# system_prompt: "Today is #{Date.today}. Current promotion: 20% off."
|
|
45
|
-
# )
|
|
29
|
+
# @example Bare robot configured via chaining
|
|
30
|
+
# robot = Robot.new(name: "bot")
|
|
31
|
+
# robot.with_instructions("Be concise.").run("Hello")
|
|
46
32
|
#
|
|
47
33
|
# @example Robot with tools
|
|
48
34
|
# robot = Robot.new(
|
|
49
35
|
# name: "support",
|
|
50
36
|
# template: :support,
|
|
51
|
-
#
|
|
52
|
-
# tools: [OrderLookup, RefundProcessor]
|
|
53
|
-
# )
|
|
54
|
-
#
|
|
55
|
-
# @example Robot with hierarchical MCP/tools config
|
|
56
|
-
# robot = Robot.new(
|
|
57
|
-
# name: "assistant",
|
|
58
|
-
# template: :assistant,
|
|
59
|
-
# mcp: :inherit, # Inherit from network/config
|
|
60
|
-
# tools: %w[search_code] # Only allow search_code tool
|
|
37
|
+
# local_tools: [OrderLookup, RefundProcessor]
|
|
61
38
|
# )
|
|
62
39
|
#
|
|
63
|
-
class Robot
|
|
40
|
+
class Robot < RubyLLM::Agent
|
|
41
|
+
include Robot::TemplateRendering
|
|
42
|
+
include Robot::MCPManagement
|
|
43
|
+
include Robot::BusMessaging
|
|
44
|
+
|
|
64
45
|
# @!attribute [r] name
|
|
65
46
|
# @return [String] the unique identifier for the robot
|
|
66
47
|
# @!attribute [r] description
|
|
67
48
|
# @return [String, nil] an optional description of the robot's purpose
|
|
68
49
|
# @!attribute [r] template
|
|
69
|
-
# @return [Symbol, nil] the
|
|
50
|
+
# @return [Symbol, nil] the prompt_manager template for the robot's prompt
|
|
70
51
|
# @!attribute [r] system_prompt
|
|
71
52
|
# @return [String, nil] inline system prompt (used alone or appended to template)
|
|
72
|
-
# @!attribute [r] model
|
|
73
|
-
# @return [String, Object] the LLM model identifier or model object
|
|
74
53
|
# @!attribute [r] local_tools
|
|
75
54
|
# @return [Array] the locally defined tools for this robot
|
|
76
55
|
# @!attribute [r] mcp_clients
|
|
@@ -79,7 +58,15 @@ module RobotLab
|
|
|
79
58
|
# @return [Array<Tool>] tools discovered from MCP servers
|
|
80
59
|
# @!attribute [r] memory
|
|
81
60
|
# @return [Memory] the robot's inherent memory (used when standalone, not in network)
|
|
82
|
-
|
|
61
|
+
# @!attribute [rw] input
|
|
62
|
+
# @return [IO] input stream for user interaction (default: $stdin)
|
|
63
|
+
# @!attribute [rw] output
|
|
64
|
+
# @return [IO] output stream for user interaction (default: $stdout)
|
|
65
|
+
attr_accessor :input, :output
|
|
66
|
+
|
|
67
|
+
attr_reader :name, :description, :template, :system_prompt,
|
|
68
|
+
:local_tools, :mcp_clients, :mcp_tools, :memory,
|
|
69
|
+
:bus, :outbox, :config
|
|
83
70
|
|
|
84
71
|
# @!attribute [r] mcp_config
|
|
85
72
|
# @return [Symbol, Array] build-time MCP configuration (raw, unresolved)
|
|
@@ -90,37 +77,27 @@ module RobotLab
|
|
|
90
77
|
# Creates a new Robot instance.
|
|
91
78
|
#
|
|
92
79
|
# @param name [String] the unique identifier for the robot
|
|
93
|
-
# @param template [Symbol, nil] the
|
|
94
|
-
# @param system_prompt [String, nil] inline system prompt
|
|
95
|
-
# @param context [Hash, Proc] variables to pass to the template
|
|
96
|
-
# @param description [String, nil]
|
|
97
|
-
# @param local_tools [Array] tools defined locally
|
|
98
|
-
# @param model [String, nil] the LLM model to use
|
|
80
|
+
# @param template [Symbol, nil] the prompt_manager template
|
|
81
|
+
# @param system_prompt [String, nil] inline system prompt
|
|
82
|
+
# @param context [Hash, Proc] variables to pass to the template
|
|
83
|
+
# @param description [String, nil] optional description
|
|
84
|
+
# @param local_tools [Array] tools defined locally
|
|
85
|
+
# @param model [String, nil] the LLM model to use
|
|
99
86
|
# @param mcp_servers [Array] legacy parameter for MCP server configurations
|
|
100
|
-
# @param mcp [Symbol, Array] hierarchical MCP config
|
|
101
|
-
# @param tools [Symbol, Array] hierarchical tools config
|
|
87
|
+
# @param mcp [Symbol, Array] hierarchical MCP config
|
|
88
|
+
# @param tools [Symbol, Array] hierarchical tools config
|
|
102
89
|
# @param on_tool_call [Proc, nil] callback invoked when a tool is called
|
|
103
90
|
# @param on_tool_result [Proc, nil] callback invoked when a tool returns a result
|
|
104
|
-
# @param enable_cache [Boolean] whether to enable semantic caching
|
|
105
|
-
#
|
|
106
|
-
# @
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
# @
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
# @
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
# @example Robot with tools and callbacks
|
|
116
|
-
# Robot.new(
|
|
117
|
-
# name: "support",
|
|
118
|
-
# template: :support,
|
|
119
|
-
# local_tools: [OrderLookup],
|
|
120
|
-
# on_tool_call: ->(call) { puts "Calling #{call.name}" }
|
|
121
|
-
# )
|
|
122
|
-
#
|
|
123
|
-
# @raise [ArgumentError] if neither template nor system_prompt is provided
|
|
91
|
+
# @param enable_cache [Boolean] whether to enable semantic caching
|
|
92
|
+
# @param bus [TypedBus::MessageBus, nil] optional message bus for inter-robot communication
|
|
93
|
+
# @param temperature [Float, nil] controls randomness
|
|
94
|
+
# @param top_p [Float, nil] nucleus sampling threshold
|
|
95
|
+
# @param top_k [Integer, nil] top-k sampling
|
|
96
|
+
# @param max_tokens [Integer, nil] maximum tokens in response
|
|
97
|
+
# @param presence_penalty [Float, nil] penalize based on presence
|
|
98
|
+
# @param frequency_penalty [Float, nil] penalize based on frequency
|
|
99
|
+
# @param stop [String, Array, nil] stop sequences
|
|
100
|
+
# @param config [RunConfig, nil] shared configuration (merged with explicit kwargs)
|
|
124
101
|
def initialize(
|
|
125
102
|
name:,
|
|
126
103
|
template: nil,
|
|
@@ -134,71 +111,138 @@ module RobotLab
|
|
|
134
111
|
tools: :none,
|
|
135
112
|
on_tool_call: nil,
|
|
136
113
|
on_tool_result: nil,
|
|
137
|
-
enable_cache: true
|
|
114
|
+
enable_cache: true,
|
|
115
|
+
bus: nil,
|
|
116
|
+
temperature: nil,
|
|
117
|
+
top_p: nil,
|
|
118
|
+
top_k: nil,
|
|
119
|
+
max_tokens: nil,
|
|
120
|
+
presence_penalty: nil,
|
|
121
|
+
frequency_penalty: nil,
|
|
122
|
+
stop: nil,
|
|
123
|
+
config: nil
|
|
138
124
|
)
|
|
139
|
-
unless template || system_prompt
|
|
140
|
-
raise ArgumentError, "Must provide either template or system_prompt"
|
|
141
|
-
end
|
|
142
|
-
|
|
143
125
|
@name = name.to_s
|
|
126
|
+
@name_from_constructor = (name.to_s != "robot")
|
|
144
127
|
@template = template
|
|
145
128
|
@system_prompt = system_prompt
|
|
146
129
|
@build_context = context
|
|
147
130
|
@description = description
|
|
148
131
|
@local_tools = Array(local_tools)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
152
154
|
|
|
153
155
|
# Store raw config values for hierarchical resolution
|
|
154
|
-
|
|
155
|
-
@
|
|
156
|
-
@tools_config = tools
|
|
156
|
+
@mcp_config = @config.mcp || :none
|
|
157
|
+
@tools_config = @config.tools || :none
|
|
157
158
|
|
|
158
159
|
# MCP state
|
|
159
160
|
@mcp_clients = {}
|
|
160
161
|
@mcp_tools = []
|
|
161
162
|
@mcp_initialized = false
|
|
162
163
|
|
|
164
|
+
# Bus state (optional inter-robot communication)
|
|
165
|
+
@bus = @config.bus
|
|
166
|
+
@message_counter = 0
|
|
167
|
+
@outbox = {}
|
|
168
|
+
@message_handler = nil
|
|
169
|
+
@bus_processing = false
|
|
170
|
+
@bus_queue = []
|
|
171
|
+
|
|
163
172
|
# Inherent memory (used when standalone, not in a network)
|
|
164
|
-
|
|
173
|
+
cache_enabled = @config.key?(:enable_cache) ? @config.enable_cache : true
|
|
174
|
+
@memory = Memory.new(enable_cache: cache_enabled)
|
|
175
|
+
|
|
176
|
+
# Ensure config is loaded (triggers PM setup, RubyLLM config, etc.)
|
|
177
|
+
config = RobotLab.config
|
|
178
|
+
|
|
179
|
+
# Build chat kwargs for Agent's super
|
|
180
|
+
resolved_model = @config.model || config.ruby_llm.model
|
|
181
|
+
chat_kwargs = { model: resolved_model }
|
|
182
|
+
|
|
183
|
+
# Create the persistent chat via Agent's initialize
|
|
184
|
+
super(chat: nil, **chat_kwargs)
|
|
185
|
+
|
|
186
|
+
# Apply template first (includes front matter config like model, temperature)
|
|
187
|
+
# then constructor params override — constructor is more specific than template.
|
|
188
|
+
apply_template_to_chat(context) if @template
|
|
189
|
+
@chat.with_instructions(@system_prompt) if @system_prompt
|
|
190
|
+
|
|
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)
|
|
199
|
+
|
|
200
|
+
# Apply callbacks
|
|
201
|
+
@chat.on_tool_call(&@on_tool_call) if @on_tool_call
|
|
202
|
+
@chat.on_tool_result(&@on_tool_result) if @on_tool_result
|
|
203
|
+
|
|
204
|
+
# Set up bus channel if a bus was provided
|
|
205
|
+
setup_bus_channel if @bus
|
|
165
206
|
end
|
|
166
207
|
|
|
167
|
-
|
|
168
|
-
#
|
|
169
|
-
# Provided for backward compatibility with earlier API versions.
|
|
208
|
+
|
|
209
|
+
# Returns the model identifier
|
|
170
210
|
#
|
|
171
|
-
# @return [
|
|
172
|
-
def
|
|
173
|
-
@
|
|
211
|
+
# @return [String, nil] the LLM model ID string
|
|
212
|
+
def model
|
|
213
|
+
return nil unless @chat.respond_to?(:model)
|
|
214
|
+
|
|
215
|
+
m = @chat.model
|
|
216
|
+
m.respond_to?(:id) ? m.id : m.to_s
|
|
174
217
|
end
|
|
175
218
|
|
|
176
|
-
#
|
|
219
|
+
# Forward with_* methods to the persistent chat, returning self for chaining
|
|
220
|
+
%i[
|
|
221
|
+
with_model with_temperature with_top_p with_top_k with_max_tokens
|
|
222
|
+
with_presence_penalty with_frequency_penalty with_stop
|
|
223
|
+
with_instructions with_tool with_tools with_params
|
|
224
|
+
with_headers with_schema with_context with_thinking
|
|
225
|
+
].each do |method|
|
|
226
|
+
define_method(method) do |*args, **kwargs, &block|
|
|
227
|
+
@chat.public_send(method, *args, **kwargs, &block)
|
|
228
|
+
self
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# Send a message and get a response, with Robot's extended capabilities
|
|
177
234
|
#
|
|
178
|
-
# @param
|
|
179
|
-
# @param
|
|
180
|
-
# @param
|
|
181
|
-
# @param
|
|
182
|
-
# @param
|
|
183
|
-
# @param
|
|
235
|
+
# @param message [String] the user message
|
|
236
|
+
# @param network [NetworkRun, nil] network context (legacy)
|
|
237
|
+
# @param network_memory [Memory, nil] shared network memory
|
|
238
|
+
# @param memory [Memory, Hash, nil] runtime memory to merge
|
|
239
|
+
# @param mcp [Symbol, Array, nil] runtime MCP override
|
|
240
|
+
# @param tools [Symbol, Array, nil] runtime tools override
|
|
184
241
|
# @return [RobotResult]
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
# @example Runtime memory injection
|
|
191
|
-
# robot.run(message: "Hello", memory: { user_id: 123, session: "abc" })
|
|
192
|
-
#
|
|
193
|
-
# @example With network shared memory
|
|
194
|
-
# robot.run(message: "Analyze this", network_memory: network.memory)
|
|
195
|
-
#
|
|
196
|
-
def run(network: nil, network_memory: nil, memory: nil, mcp: :none, tools: :none, **run_context)
|
|
197
|
-
# Determine which memory to use (priority order):
|
|
198
|
-
# 1. Explicit network_memory parameter
|
|
199
|
-
# 2. Network object's memory (legacy)
|
|
200
|
-
# 3. Robot's inherent memory
|
|
201
|
-
run_memory = network_memory || network&.memory || @memory
|
|
242
|
+
def run(message = nil, network: nil, network_memory: nil, network_config: nil,
|
|
243
|
+
memory: nil, mcp: :none, tools: :none, **kwargs)
|
|
244
|
+
# Determine which memory to use
|
|
245
|
+
run_memory = resolve_active_memory(network: network, network_memory: network_memory)
|
|
202
246
|
|
|
203
247
|
# Merge runtime memory if provided
|
|
204
248
|
case memory
|
|
@@ -214,90 +258,101 @@ module RobotLab
|
|
|
214
258
|
|
|
215
259
|
begin
|
|
216
260
|
# Resolve hierarchical MCP and tools configuration
|
|
217
|
-
resolved_mcp = resolve_mcp_hierarchy(mcp, network: network)
|
|
218
|
-
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)
|
|
219
263
|
|
|
220
264
|
# Initialize or update MCP clients based on resolved config
|
|
221
265
|
ensure_mcp_clients(resolved_mcp)
|
|
222
266
|
|
|
223
|
-
#
|
|
224
|
-
|
|
225
|
-
|
|
267
|
+
# Apply filtered tools to the persistent chat
|
|
268
|
+
filtered = filtered_tools(resolved_tools)
|
|
269
|
+
@chat.with_tools(*filtered) if filtered.any?
|
|
226
270
|
|
|
227
|
-
#
|
|
228
|
-
|
|
271
|
+
# Re-render template with run-time context merged into build-time context.
|
|
272
|
+
# Template parameters (e.g. customer: null) may require values that are
|
|
273
|
+
# only available at run time — the robot gathers them before rendering.
|
|
274
|
+
run_context = kwargs.except(:with)
|
|
275
|
+
rerender_template(run_context) if @template && run_context.any?
|
|
229
276
|
|
|
230
|
-
#
|
|
231
|
-
|
|
277
|
+
# Delegate to Agent's ask (which calls @chat.ask)
|
|
278
|
+
ask_kwargs = kwargs.slice(:with)
|
|
279
|
+
response = ask(message, **ask_kwargs)
|
|
232
280
|
|
|
233
281
|
build_result(response, run_memory)
|
|
234
282
|
ensure
|
|
235
|
-
# Restore previous writer
|
|
236
283
|
run_memory.current_writer = previous_writer
|
|
237
284
|
end
|
|
238
285
|
end
|
|
239
286
|
|
|
240
|
-
|
|
287
|
+
|
|
288
|
+
# Reconfigure the robot for a new context
|
|
241
289
|
#
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
#
|
|
290
|
+
# @param template [Symbol, nil] new template to apply
|
|
291
|
+
# @param context [Hash, nil] new context for the template
|
|
292
|
+
# @param system_prompt [String, nil] new system prompt
|
|
293
|
+
# @param model [String, nil] new model
|
|
294
|
+
# @param temperature [Float, nil] new temperature
|
|
295
|
+
# @return [self]
|
|
296
|
+
def update(template: nil, context: nil, system_prompt: nil, model: nil, temperature: nil, **kwargs)
|
|
297
|
+
if template
|
|
298
|
+
@template = template
|
|
299
|
+
ctx = context || @build_context
|
|
300
|
+
apply_template_to_chat(ctx)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
@chat.with_instructions(system_prompt) if system_prompt
|
|
304
|
+
@chat.with_model(model) if model
|
|
305
|
+
apply_chat_option(:with_temperature, temperature)
|
|
306
|
+
|
|
307
|
+
kwargs.each do |key, value|
|
|
308
|
+
method = :"with_#{key}"
|
|
309
|
+
@chat.public_send(method, value) if value && @chat.respond_to?(method)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
self
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# SimpleFlow step interface
|
|
245
317
|
#
|
|
246
318
|
# @param result [SimpleFlow::Result] incoming result from previous step
|
|
247
319
|
# @return [SimpleFlow::Result] result with robot output
|
|
248
|
-
#
|
|
249
|
-
# @example Using a robot as a pipeline step
|
|
250
|
-
# pipeline = SimpleFlow::Pipeline.new do
|
|
251
|
-
# step :classifier, classifier_robot, depends_on: :none
|
|
252
|
-
# step :billing, billing_robot, depends_on: :optional
|
|
253
|
-
# end
|
|
254
|
-
#
|
|
255
320
|
def call(result)
|
|
256
|
-
|
|
321
|
+
run_context = extract_run_context(result)
|
|
322
|
+
|
|
323
|
+
# Extract the message from run context
|
|
324
|
+
message = run_context.delete(:message)
|
|
325
|
+
|
|
326
|
+
robot_result = run(message, **run_context)
|
|
257
327
|
|
|
258
328
|
result
|
|
259
329
|
.with_context(@name.to_sym, robot_result)
|
|
260
330
|
.continue(robot_result)
|
|
261
331
|
end
|
|
262
332
|
|
|
333
|
+
|
|
263
334
|
# Reset the robot's inherent memory
|
|
264
335
|
#
|
|
265
|
-
# NOTE: This only affects the robot's standalone memory. When a robot runs
|
|
266
|
-
# as part of a network, it uses the network's shared memory instead.
|
|
267
|
-
# To reset memory for network execution, use `network.reset_memory`.
|
|
268
|
-
#
|
|
269
336
|
# @return [self]
|
|
270
|
-
#
|
|
271
|
-
# @example Standalone robot
|
|
272
|
-
# robot.run(message: "My name is Alice")
|
|
273
|
-
# robot.reset_memory # Clears the conversation
|
|
274
|
-
# robot.run(message: "What's my name?") # Won't remember Alice
|
|
275
|
-
#
|
|
276
|
-
# @example Robot in network (reset_memory has no effect on network runs)
|
|
277
|
-
# network.run(message: "Hello")
|
|
278
|
-
# robot.reset_memory # Does NOT affect network memory
|
|
279
|
-
# network.run(message: "Hi") # Network memory still intact
|
|
280
|
-
# network.reset_memory # Use this to reset network memory
|
|
281
|
-
#
|
|
282
337
|
def reset_memory
|
|
283
338
|
@memory.reset
|
|
284
339
|
self
|
|
285
340
|
end
|
|
286
341
|
|
|
287
|
-
|
|
288
|
-
#
|
|
289
|
-
# Call this method when done using the robot to clean up MCP connections.
|
|
342
|
+
|
|
343
|
+
# Disconnect all MCP clients and bus channel.
|
|
290
344
|
#
|
|
291
345
|
# @return [self]
|
|
292
|
-
#
|
|
293
346
|
def disconnect
|
|
294
347
|
@mcp_clients.each_value(&:disconnect)
|
|
348
|
+
teardown_bus_channel if @bus
|
|
295
349
|
self
|
|
296
350
|
end
|
|
297
351
|
|
|
298
|
-
|
|
352
|
+
|
|
353
|
+
# Converts the robot to a hash representation
|
|
299
354
|
#
|
|
300
|
-
# @return [Hash]
|
|
355
|
+
# @return [Hash]
|
|
301
356
|
def to_h
|
|
302
357
|
{
|
|
303
358
|
name: name,
|
|
@@ -309,28 +364,36 @@ module RobotLab
|
|
|
309
364
|
mcp_config: @mcp_config,
|
|
310
365
|
tools_config: @tools_config,
|
|
311
366
|
mcp_servers: @mcp_clients.keys,
|
|
312
|
-
model: model
|
|
367
|
+
model: model,
|
|
368
|
+
config: (@config.empty? ? nil : @config.to_json_hash),
|
|
369
|
+
bus: @bus ? true : nil
|
|
313
370
|
}.compact
|
|
314
371
|
end
|
|
315
372
|
|
|
316
373
|
private
|
|
317
374
|
|
|
375
|
+
# Apply a chat option if the value is non-nil
|
|
376
|
+
def apply_chat_option(method, value)
|
|
377
|
+
@chat.public_send(method, value) if value
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# Determine which memory to use
|
|
382
|
+
def resolve_active_memory(network: nil, network_memory: nil)
|
|
383
|
+
network_memory || network&.memory || @memory
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
|
|
318
387
|
# Extract run context from SimpleFlow::Result
|
|
319
|
-
#
|
|
320
|
-
# Merges original run params (preserved in context) with current value.
|
|
321
|
-
# Extracts special parameters (mcp, tools, memory, network_memory) for Robot#run.
|
|
322
|
-
#
|
|
323
|
-
# @param result [SimpleFlow::Result] the incoming result
|
|
324
|
-
# @return [Hash] context for run method including mcp/tools config
|
|
325
|
-
#
|
|
326
388
|
def extract_run_context(result)
|
|
327
389
|
run_params = result.context[:run_params] || {}
|
|
328
390
|
|
|
329
|
-
# Extract robot-specific params
|
|
391
|
+
# Extract robot-specific params
|
|
330
392
|
mcp = run_params.delete(:mcp) || :none
|
|
331
393
|
tools = run_params.delete(:tools) || :none
|
|
332
394
|
memory = run_params.delete(:memory)
|
|
333
395
|
network_memory = run_params.delete(:network_memory)
|
|
396
|
+
network_config = run_params.delete(:network_config)
|
|
334
397
|
|
|
335
398
|
# Build base context from remaining run params
|
|
336
399
|
base = run_params.dup
|
|
@@ -347,58 +410,20 @@ module RobotLab
|
|
|
347
410
|
base.merge(message: result.value.to_s)
|
|
348
411
|
end
|
|
349
412
|
|
|
350
|
-
# Add back the special params
|
|
413
|
+
# Add back the special params
|
|
351
414
|
merged[:mcp] = mcp
|
|
352
415
|
merged[:tools] = tools
|
|
353
416
|
merged[:memory] = memory if memory
|
|
354
417
|
merged[:network_memory] = network_memory if network_memory
|
|
418
|
+
merged[:network_config] = network_config if network_config
|
|
355
419
|
|
|
356
420
|
merged
|
|
357
421
|
end
|
|
358
422
|
|
|
359
|
-
def resolve_context(context, network:)
|
|
360
|
-
case context
|
|
361
|
-
when Proc then context.call(network: network)
|
|
362
|
-
when Hash then context
|
|
363
|
-
else {}
|
|
364
|
-
end
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
def build_chat(context, allowed_tools:, memory:)
|
|
368
|
-
model_id = @model.respond_to?(:model_id) ? @model.model_id : @model.to_s
|
|
369
|
-
|
|
370
|
-
chat = RubyLLM.chat(model: model_id)
|
|
371
|
-
|
|
372
|
-
# Apply template and/or system_prompt
|
|
373
|
-
# - Template only: use with_template
|
|
374
|
-
# - system_prompt only: use with_instructions
|
|
375
|
-
# - Both: use with_template, then append with_instructions
|
|
376
|
-
if @template
|
|
377
|
-
chat = chat.with_template(@template, **context)
|
|
378
|
-
chat = chat.with_instructions(@system_prompt) if @system_prompt
|
|
379
|
-
else
|
|
380
|
-
chat = chat.with_instructions(@system_prompt)
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
# Get filtered tools based on whitelist
|
|
384
|
-
filtered = filtered_tools(allowed_tools)
|
|
385
|
-
chat = chat.with_tools(*filtered) if filtered.any?
|
|
386
|
-
|
|
387
|
-
# Add callbacks if provided
|
|
388
|
-
chat = chat.on_tool_call(&@on_tool_call) if @on_tool_call
|
|
389
|
-
chat = chat.on_tool_result(&@on_tool_result) if @on_tool_result
|
|
390
|
-
|
|
391
|
-
# NOTE: Semantic cache wrapping is disabled because the SemanticCache::Middleware
|
|
392
|
-
# only supports `ask` method, not `complete`. The caching feature needs to be
|
|
393
|
-
# re-designed to use the `ask` interface or the `fetch` pattern.
|
|
394
|
-
# See: https://github.com/ruby-llm/ruby_llm-semantic_cache
|
|
395
|
-
|
|
396
|
-
chat
|
|
397
|
-
end
|
|
398
423
|
|
|
399
424
|
def build_result(response, _memory)
|
|
400
425
|
output = if response.respond_to?(:content) && response.content
|
|
401
|
-
[TextMessage.new(role:
|
|
426
|
+
[TextMessage.new(role: 'assistant', content: response.content)]
|
|
402
427
|
else
|
|
403
428
|
[]
|
|
404
429
|
end
|
|
@@ -413,6 +438,7 @@ module RobotLab
|
|
|
413
438
|
)
|
|
414
439
|
end
|
|
415
440
|
|
|
441
|
+
|
|
416
442
|
def normalize_tool_calls(tool_calls)
|
|
417
443
|
return [] unless tool_calls
|
|
418
444
|
|
|
@@ -420,7 +446,7 @@ module RobotLab
|
|
|
420
446
|
if tc.is_a?(Hash)
|
|
421
447
|
ToolResultMessage.new(
|
|
422
448
|
tool: tc,
|
|
423
|
-
content: tc[:result] || tc[
|
|
449
|
+
content: tc[:result] || tc['result']
|
|
424
450
|
)
|
|
425
451
|
else
|
|
426
452
|
tc
|
|
@@ -428,128 +454,12 @@ module RobotLab
|
|
|
428
454
|
end
|
|
429
455
|
end
|
|
430
456
|
|
|
431
|
-
# Resolve MCP hierarchy: runtime -> robot build -> network -> config
|
|
432
|
-
#
|
|
433
|
-
# @param runtime_value [Symbol, Array, nil] Runtime MCP override
|
|
434
|
-
# @param network [NetworkRun, nil] Network context
|
|
435
|
-
# @return [Array] Resolved MCP server configurations
|
|
436
|
-
#
|
|
437
|
-
def resolve_mcp_hierarchy(runtime_value, network:)
|
|
438
|
-
# Get parent value (network or config)
|
|
439
|
-
parent_value = network&.network&.mcp || RobotLab.configuration.mcp
|
|
440
|
-
|
|
441
|
-
# Resolve robot build config against parent
|
|
442
|
-
build_resolved = ToolConfig.resolve_mcp(@mcp_config, parent_value: parent_value)
|
|
443
|
-
|
|
444
|
-
# Resolve runtime against build
|
|
445
|
-
ToolConfig.resolve_mcp(runtime_value, parent_value: build_resolved)
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
# Resolve tools hierarchy: runtime -> robot build -> network -> config
|
|
449
|
-
#
|
|
450
|
-
# @param runtime_value [Symbol, Array, nil] Runtime tools override
|
|
451
|
-
# @param network [NetworkRun, nil] Network context
|
|
452
|
-
# @return [Array<String>] Resolved tool names whitelist
|
|
453
|
-
#
|
|
454
|
-
def resolve_tools_hierarchy(runtime_value, network:)
|
|
455
|
-
# Get parent value (network or config)
|
|
456
|
-
parent_value = network&.network&.tools || RobotLab.configuration.tools
|
|
457
|
-
|
|
458
|
-
# Resolve robot build config against parent
|
|
459
|
-
build_resolved = ToolConfig.resolve_tools(@tools_config, parent_value: parent_value)
|
|
460
|
-
|
|
461
|
-
# Resolve runtime against build
|
|
462
|
-
ToolConfig.resolve_tools(runtime_value, parent_value: build_resolved)
|
|
463
|
-
end
|
|
464
|
-
|
|
465
|
-
# Ensure MCP clients are initialized for the given server configs
|
|
466
|
-
#
|
|
467
|
-
# @param mcp_servers [Array] MCP server configurations
|
|
468
|
-
#
|
|
469
|
-
def ensure_mcp_clients(mcp_servers)
|
|
470
|
-
return if mcp_servers.empty?
|
|
471
|
-
|
|
472
|
-
# Get server names from configs
|
|
473
|
-
needed_servers = mcp_servers.map { |s| s.is_a?(Hash) ? s[:name] : s.to_s }.compact
|
|
474
|
-
|
|
475
|
-
# Skip if already initialized with same servers
|
|
476
|
-
return if @mcp_initialized && (@mcp_clients.keys.sort == needed_servers.sort)
|
|
477
|
-
|
|
478
|
-
# Disconnect existing clients if config changed
|
|
479
|
-
disconnect if @mcp_initialized
|
|
480
|
-
|
|
481
|
-
# Initialize new clients
|
|
482
|
-
@mcp_clients = {}
|
|
483
|
-
@mcp_tools = []
|
|
484
|
-
|
|
485
|
-
mcp_servers.each do |server_config|
|
|
486
|
-
init_mcp_client(server_config)
|
|
487
|
-
end
|
|
488
|
-
|
|
489
|
-
@mcp_initialized = true
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
# Initialize a single MCP client
|
|
493
|
-
#
|
|
494
|
-
# @param server_config [Hash] MCP server configuration
|
|
495
|
-
#
|
|
496
|
-
def init_mcp_client(server_config)
|
|
497
|
-
client = MCP::Client.new(server_config)
|
|
498
|
-
client.connect
|
|
499
|
-
|
|
500
|
-
if client.connected?
|
|
501
|
-
server_name = client.server.name
|
|
502
|
-
@mcp_clients[server_name] = client
|
|
503
|
-
discover_mcp_tools(client, server_name)
|
|
504
|
-
else
|
|
505
|
-
RobotLab.configuration.logger.warn(
|
|
506
|
-
"Robot '#{@name}' failed to connect to MCP server: #{server_config[:name] || server_config}"
|
|
507
|
-
)
|
|
508
|
-
end
|
|
509
|
-
end
|
|
510
|
-
|
|
511
|
-
# Discover tools from an MCP server and add them to @mcp_tools
|
|
512
|
-
#
|
|
513
|
-
# @param client [MCP::Client] Connected MCP client
|
|
514
|
-
# @param server_name [String] Name of the MCP server
|
|
515
|
-
#
|
|
516
|
-
def discover_mcp_tools(client, server_name)
|
|
517
|
-
tools = client.list_tools
|
|
518
|
-
|
|
519
|
-
tools.each do |tool_def|
|
|
520
|
-
tool_name = tool_def[:name]
|
|
521
|
-
mcp_client = client
|
|
522
|
-
|
|
523
|
-
# Create a Tool that delegates to the MCP client
|
|
524
|
-
tool = Tool.new(
|
|
525
|
-
name: tool_name,
|
|
526
|
-
description: tool_def[:description],
|
|
527
|
-
parameters: tool_def[:inputSchema],
|
|
528
|
-
mcp: server_name,
|
|
529
|
-
handler: ->(input, **_opts) { mcp_client.call_tool(tool_name, input) }
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
@mcp_tools << tool
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
RobotLab.configuration.logger.info(
|
|
536
|
-
"Robot '#{@name}' discovered #{tools.size} tools from MCP server '#{server_name}'"
|
|
537
|
-
)
|
|
538
|
-
end
|
|
539
457
|
|
|
540
|
-
# Get all tools (local + MCP)
|
|
541
|
-
#
|
|
542
|
-
# @return [Array] Combined array of local and MCP tools
|
|
543
|
-
#
|
|
544
458
|
def all_tools
|
|
545
459
|
@local_tools + @mcp_tools
|
|
546
460
|
end
|
|
547
461
|
|
|
548
|
-
|
|
549
|
-
#
|
|
550
|
-
# @param allowed_names [Array<String>] Whitelist of tool names (empty = all allowed)
|
|
551
|
-
# @return [Array] Filtered tools
|
|
552
|
-
#
|
|
462
|
+
|
|
553
463
|
def filtered_tools(allowed_names)
|
|
554
464
|
available = all_tools
|
|
555
465
|
return available if allowed_names.empty?
|