robot_lab 0.5.1 → 0.6.0
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 +11 -0
- data/lib/robot_lab/mcp/registry.rb +2 -1
- data/lib/robot_lab/mcp/server_discovery.rb +7 -1
- data/lib/robot_lab/mcp.rb +26 -5
- data/lib/robot_lab/robot/mcp_management.rb +1 -1
- data/lib/robot_lab/robot/template_rendering.rb +1 -2
- data/lib/robot_lab/robot.rb +1 -1
- data/lib/robot_lab/tool_config.rb +17 -1
- data/lib/robot_lab/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0a38d5eaca5ddd1d558581dd57db81c94c461b0d9bf7bffbbf325f529e5a8ef
|
|
4
|
+
data.tar.gz: 7541f4d1fd133333d667a528f8f96da5287a76a4c6642a3677334496740934b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6501bd24004fc28a105555a74ca5ae40d4e713d4485d1c581bfb6a77e810119b8e5a9c939c29fd19ec770e7b51eb10b43825c6ebddb7ef34fd60c4915e93639d
|
|
7
|
+
data.tar.gz: 70b65d1afb7fa35c48c278455ed7dd33fb7ff3c506eaf259ae885bb2b681efbeb0f4d9d8dfa7f99676817f07328c40ca7428ddf0ffe2110503bdf290524bf65c
|
data/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
### Fixed
|
|
12
12
|
|
|
13
|
+
- **Tool classes in `local_tools:`.** A `RubyLLM::Tool` subclass passed as a class (`local_tools: [SharedTools::Tools::DnsTool]`) reached the chat as `dns`, but robot_lab's own checks used the Ruby class name `"SharedTools::Tools::DnsTool"`. As a result, a `tools: %w[dns]` allowlist dropped the tool without an error. The robot now instantiates tool classes on construction, so every check uses the tool's name. The new `ToolConfig.instantiate` does this here and for template `tools:` front matter, and `ToolConfig.tool_name` (now public) reads `tool_name` from a tool class.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **MCP server classes by constant name.** `RobotLab::MCP.build`, `name_of`, `Registry#add` and a robot's per-turn server selection accept a String naming a `RubyLLM::MCP` subclass. Template front matter can now list `mcp: ["SharedTools::MCP::GitHub"]` in the same way `tools:` lists tool classes. The new `RobotLab::MCP.resolve` does the lookup and raises `MCPError` for an undefined constant or a class that is not a `RubyLLM::MCP`.
|
|
18
|
+
- **Class-level descriptions in `mcp_discovery`.** `ServerDiscovery.description_for` now falls back to a server's class when the instance has no `description`. Plain `RubyLLM::MCP` subclasses that declare a description at class level, such as the shared_tools 0.7.1 MCP clients, are scored instead of looking blank.
|
|
19
|
+
|
|
20
|
+
## [0.5.1] - 2026-11-01
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
13
24
|
- **OAuth `grant:` from YAML.** An MCP server config such as `oauth: { grant: client_credentials }` raised `ArgumentError: Unknown OAuth grant`, because ruby_llm accepts the grant only as a Symbol. `RobotLab::MCP.define` now converts a String grant to a Symbol.
|
|
14
25
|
|
|
15
26
|
## [0.5.0] - 2026-11-01
|
|
@@ -24,9 +24,10 @@ module RobotLab
|
|
|
24
24
|
|
|
25
25
|
# Register a server (or return the one already registered for +spec+).
|
|
26
26
|
#
|
|
27
|
-
# @param spec [Hash, RubyLLM::MCP, Class<RubyLLM::MCP
|
|
27
|
+
# @param spec [Hash, RubyLLM::MCP, Class<RubyLLM::MCP>, String]
|
|
28
28
|
# @return [RubyLLM::MCP]
|
|
29
29
|
def add(spec)
|
|
30
|
+
spec = MCP.resolve(spec)
|
|
30
31
|
key = MCP.name_of(spec).to_sym
|
|
31
32
|
existing = @servers[key]
|
|
32
33
|
return existing if existing && same_spec?(key, spec, existing)
|
|
@@ -101,12 +101,18 @@ module RobotLab
|
|
|
101
101
|
name.to_s.strip
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# An instance without its own +description+ falls back to its class's,
|
|
105
|
+
# so plain +RubyLLM::MCP+ subclasses that declare one at class level
|
|
106
|
+
# (SharedTools::MCP::GitHub, ...) are scored too.
|
|
107
|
+
#
|
|
104
108
|
# @param server [Hash, RubyLLM::MCP, Class<RubyLLM::MCP>]
|
|
105
109
|
# @return [String] description or empty string
|
|
106
110
|
def self.description_for(server)
|
|
107
111
|
desc = case server
|
|
108
112
|
when Hash then server[:description] || server['description']
|
|
109
|
-
else
|
|
113
|
+
else
|
|
114
|
+
owner = [server, server.class].find { it.respond_to?(:description) }
|
|
115
|
+
owner&.description
|
|
110
116
|
end
|
|
111
117
|
desc.to_s.strip
|
|
112
118
|
end
|
data/lib/robot_lab/mcp.rb
CHANGED
|
@@ -54,12 +54,14 @@ module RobotLab
|
|
|
54
54
|
module_function
|
|
55
55
|
|
|
56
56
|
# Build a server from a config Hash, or pass through an instance or class.
|
|
57
|
+
# A String names a +RubyLLM::MCP+ subclass constant, the form template
|
|
58
|
+
# front matter uses (+mcp: ["SharedTools::MCP::GitHub"]+).
|
|
57
59
|
#
|
|
58
|
-
# @param spec [Hash, RubyLLM::MCP, Class<RubyLLM::MCP
|
|
60
|
+
# @param spec [Hash, RubyLLM::MCP, Class<RubyLLM::MCP>, String]
|
|
59
61
|
# @return [RubyLLM::MCP]
|
|
60
62
|
# @raise [MCPError] for an unusable spec
|
|
61
63
|
def build(spec)
|
|
62
|
-
case spec
|
|
64
|
+
case (spec = resolve(spec))
|
|
63
65
|
when RubyLLM::MCP then spec
|
|
64
66
|
when Class
|
|
65
67
|
raise MCPError, "#{spec} is not a RubyLLM::MCP subclass" unless spec <= RubyLLM::MCP
|
|
@@ -68,10 +70,29 @@ module RobotLab
|
|
|
68
70
|
when Hash then define(spec).new
|
|
69
71
|
else
|
|
70
72
|
raise MCPError, "Cannot build an MCP server from #{spec.class}; " \
|
|
71
|
-
'pass a Hash, a RubyLLM::MCP,
|
|
73
|
+
'pass a Hash, a RubyLLM::MCP, a RubyLLM::MCP subclass, or its constant name'
|
|
72
74
|
end
|
|
73
75
|
end
|
|
74
76
|
|
|
77
|
+
# Turn a String spec into the +RubyLLM::MCP+ subclass it names; any other
|
|
78
|
+
# spec is returned unchanged.
|
|
79
|
+
#
|
|
80
|
+
# @param spec [Hash, RubyLLM::MCP, Class<RubyLLM::MCP>, String]
|
|
81
|
+
# @return [Hash, RubyLLM::MCP, Class<RubyLLM::MCP>]
|
|
82
|
+
# @raise [MCPError] when the String does not name a RubyLLM::MCP subclass
|
|
83
|
+
def resolve(spec)
|
|
84
|
+
return spec unless spec.is_a?(String)
|
|
85
|
+
|
|
86
|
+
klass = begin
|
|
87
|
+
Object.const_get(spec)
|
|
88
|
+
rescue NameError
|
|
89
|
+
raise MCPError, "MCP server class '#{spec}' is not defined; require the file that defines it"
|
|
90
|
+
end
|
|
91
|
+
raise MCPError, "#{spec} is not a RubyLLM::MCP subclass" unless klass.is_a?(Class) && klass <= RubyLLM::MCP
|
|
92
|
+
|
|
93
|
+
klass
|
|
94
|
+
end
|
|
95
|
+
|
|
75
96
|
# Turn a config Hash into an anonymous {Server} subclass with ruby_llm's
|
|
76
97
|
# class-level DSL applied. Public so hosts can build classes once and
|
|
77
98
|
# instantiate them per request (ruby_llm's recommended pattern).
|
|
@@ -88,11 +109,11 @@ module RobotLab
|
|
|
88
109
|
|
|
89
110
|
# The name a spec will be registered under.
|
|
90
111
|
#
|
|
91
|
-
# @param spec [Hash, RubyLLM::MCP, Class<RubyLLM::MCP
|
|
112
|
+
# @param spec [Hash, RubyLLM::MCP, Class<RubyLLM::MCP>, String]
|
|
92
113
|
# @return [String]
|
|
93
114
|
# @raise [MCPError] when the spec has no name
|
|
94
115
|
def name_of(spec)
|
|
95
|
-
name = case spec
|
|
116
|
+
name = case (spec = resolve(spec))
|
|
96
117
|
when Hash then spec[:name] || spec['name']
|
|
97
118
|
when Class then spec.default_name
|
|
98
119
|
when RubyLLM::MCP then spec.name
|
|
@@ -136,7 +136,7 @@ module RobotLab
|
|
|
136
136
|
# @param message [String, RubyLLM::MCP::Prompt, nil] this run's request
|
|
137
137
|
# @return [Array<RubyLLM::MCP>]
|
|
138
138
|
def servers_for(resolved, message: nil)
|
|
139
|
-
specs = Array(resolved)
|
|
139
|
+
specs = Array(resolved).map { MCP.resolve(it) }
|
|
140
140
|
specs = MCP::ServerDiscovery.select(discovery_query(message), from: specs) if @mcp_discovery && message
|
|
141
141
|
specs.map { |spec| @mcp_registry.add(spec) }
|
|
142
142
|
end
|
|
@@ -323,8 +323,7 @@ module RobotLab
|
|
|
323
323
|
case name
|
|
324
324
|
when String
|
|
325
325
|
begin
|
|
326
|
-
|
|
327
|
-
const.is_a?(Class) && const < RubyLLM::Tool ? const.new : const
|
|
326
|
+
ToolConfig.instantiate(Object.const_get(name))
|
|
328
327
|
rescue NameError
|
|
329
328
|
RobotLab.config.logger.warn("Robot '#{@name}': tool '#{name}' not found, skipping")
|
|
330
329
|
nil
|
data/lib/robot_lab/robot.rb
CHANGED
|
@@ -565,7 +565,7 @@ module RobotLab
|
|
|
565
565
|
@system_prompt = system_prompt
|
|
566
566
|
@build_context = context
|
|
567
567
|
@description = description
|
|
568
|
-
@local_tools = Array(local_tools)
|
|
568
|
+
@local_tools = Array(local_tools).map { ToolConfig.instantiate(it) }
|
|
569
569
|
@skills = skills ? Array(skills).map(&:to_sym) : nil
|
|
570
570
|
@expanded_skills = nil
|
|
571
571
|
@pending_agent_skills = []
|
|
@@ -98,12 +98,28 @@ module RobotLab
|
|
|
98
98
|
tools.select { |tool| allowed_set.include?(tool_name(tool)) }
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
# Turn a RubyLLM::Tool subclass into an instance; anything else is
|
|
102
|
+
# returned unchanged. Robots hold tool instances so every name check
|
|
103
|
+
# sees the tool's name (+current_date_time+), not its Ruby class name
|
|
104
|
+
# (+SharedTools::Tools::CurrentDateTimeTool+).
|
|
105
|
+
#
|
|
106
|
+
# @param tool [Class<RubyLLM::Tool>, RubyLLM::Tool, Object]
|
|
107
|
+
# @return [Object]
|
|
108
|
+
#
|
|
109
|
+
def instantiate(tool)
|
|
110
|
+
tool.is_a?(Class) && tool < RubyLLM::Tool ? tool.new : tool
|
|
111
|
+
end
|
|
102
112
|
|
|
113
|
+
# The name a tool is offered to the LLM under.
|
|
114
|
+
#
|
|
115
|
+
# @param tool [String, Symbol, Class<RubyLLM::Tool>, #name]
|
|
116
|
+
# @return [String]
|
|
117
|
+
#
|
|
103
118
|
def tool_name(tool)
|
|
104
119
|
case tool
|
|
105
120
|
when String then tool
|
|
106
121
|
when Symbol then tool.to_s
|
|
122
|
+
when Class then (tool.respond_to?(:tool_name) ? tool.tool_name : tool.name).to_s
|
|
107
123
|
else tool.respond_to?(:name) ? tool.name.to_s : tool.to_s
|
|
108
124
|
end
|
|
109
125
|
end
|
data/lib/robot_lab/version.rb
CHANGED