ruby_llm-code_mode 0.2.0 → 0.3.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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -17
  3. data/lib/ruby_llm/code_mode.rb +47 -36
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b92ccf0470dc8d3563eafa55662e7aa0db058095d8ac12a0d60a062547474f81
4
- data.tar.gz: c2955740ee6b04b1a38279b25f63703194df752fa51f3d4f49166bab3f318ac3
3
+ metadata.gz: 65329e122014fe96a542cab62f9eb0bb002f3c1ac9f53fae0f5d7b384183433e
4
+ data.tar.gz: c9f4272b1b65da90128fb4a62d211a47606814091ee07095fe55535126aea154
5
5
  SHA512:
6
- metadata.gz: 7fb1029e393ec6c0041d91d5a45fbef2acdc253fe28427986d0832a73eb33b43becfa42d97983212ac7a3312a771e9506765c271bd4bd2ebcf68968b7adf467e
7
- data.tar.gz: 52e9aa1bf43f9dcedf0a6e50deb9578c86716bdd5ae353da6717a6553b7f5170f3773f5c40a4229779589be9686e0a3274788d3dc658cdb084fcc5ff862c206a
6
+ metadata.gz: 7f31a40ab23606bfd6f2c5799ff90c780c514eac95bd3f63d4e4ef933c97adf38ae7d61ca65fe4dc53ba472f8ed38a8a64ae3fcf286656c7694ac2c8c9842817
7
+ data.tar.gz: 1316c738fba0e23e1dc0db07ea63d926cb535bd082cf649a0511c68639e86e777e9077cbe7f1a9973b05e110f023ba9d4d21df3638602956525ca33a2d8e3602
data/README.md CHANGED
@@ -50,7 +50,7 @@ a fixed description built from the declared mounts:
50
50
  - a **Read-only folders** section listing each `mount` as `` `/guest/path` — description``;
51
51
  - a **Read-write folders** section listing each `mount_rw`;
52
52
  - a **Host tools** section listing every bound `RubyLLM::Tool` with its
53
- parameters, when any are bound with `tool` or `mcp`.
53
+ parameters, when any are bound with `tools` or `mcps`.
54
54
 
55
55
  The single parameter is `code` — a complete, self-contained Ruby script.
56
56
 
@@ -70,9 +70,10 @@ A JSON object sent back to the model:
70
70
  ```ruby
71
71
  mount source: host_path, dest: guest_path, description: "..." # read-only
72
72
  mount_rw source: host_path, dest: guest_path, description: "..." # read-write
73
- tool SomeRubyLLMTool # host tool
74
- tool "name" => SomeRubyLLMTool # explicit name
75
- mcp SomeMcpServer # MCP server (instance or class)
73
+ tools SomeRubyLLMTool # host tool
74
+ tools SomeTool, OtherTool # several (or arrays)
75
+ tools "name" => SomeRubyLLMTool # explicit name(s)
76
+ mcps SomeMcpServer # MCP server(s) (instance or class)
76
77
  ```
77
78
 
78
79
  - `source:` — folder on the host machine, relative to the process working
@@ -80,12 +81,15 @@ mcp SomeMcpServer # MCP server (
80
81
  - `dest:` — absolute, normalized path inside the sandbox (may not overlap
81
82
  the reserved `/work`, `/usr` or `/src` trees, and must be unique).
82
83
  - `description:` — shown to the model in the tool description.
83
- - `tool` — a `RubyLLM::Tool` class or instance; the name inside the sandbox
84
- is derived from the class-name leaf (`MyApp::Tools::Weather` → `weather`),
85
- or the explicit one from the one-pair form.
86
- - `mcp` — a `RubyLLM::MCP` instance or class (auto-instantiated, with the
87
- server's declared inputs as keywords); all of the server's tools are bound
88
- under their own names.
84
+ - `tools` — one or more `RubyLLM::Tool` classes/instances (or arrays of
85
+ them); the name inside the sandbox is derived from the class-name leaf
86
+ (`MyApp::Tools::Weather` → `weather`), or the explicit one from the
87
+ `"name" => tool` pairs. Called without arguments, it returns the bound
88
+ tools.
89
+ - `mcps` — one or more `RubyLLM::MCP` instances or classes
90
+ (auto-instantiated, with the server's declared inputs as keywords); all of
91
+ each server's tools are bound under their own names. Called without
92
+ arguments, it returns the connected servers.
89
93
 
90
94
  Invalid declarations raise at class-definition time, so a misconfigured tool
91
95
  never reaches a live chat. Subclasses inherit their parent's mounts and tools.
@@ -107,8 +111,8 @@ end
107
111
 
108
112
  class Assistant < RubyLLM::CodeMode
109
113
  mount source: "data", dest: "/data", description: "Project data"
110
- tool Weather # binds as `weather`
111
- tool "forecast" => Weather # or bind it under an explicit name
114
+ tools Weather # binds as `weather`
115
+ tools "forecast" => Weather # or bind it under an explicit name
112
116
  end
113
117
  ```
114
118
 
@@ -136,7 +140,7 @@ report = SB.call("weather", city: "Porto Alegre")
136
140
 
137
141
  ### MCP servers
138
142
 
139
- The `mcp` DSL connects [RubyLLM::MCP](https://rubyllm.com/next/mcp/) servers
143
+ The `mcps` DSL connects [RubyLLM::MCP](https://rubyllm.com/next/mcp/) servers
140
144
  and binds all their tools in one call, letting the sandboxed code reach the
141
145
  server through the host RPC channel:
142
146
 
@@ -144,9 +148,9 @@ server through the host RPC channel:
144
148
  docs = RubyLLM.mcp(url: "https://learn.microsoft.com/api/mcp")
145
149
 
146
150
  class DocsResearch < RubyLLM::CodeMode
147
- mcp docs # instance
148
- # mcp MicrosoftDocs # or the class (instantiated with .new)
149
- # mcp Linear, user: current_user # class + the server's declared inputs
151
+ mcps docs # instance
152
+ # mcps MicrosoftDocs # or the class (instantiated with .new)
153
+ # mcps Linear, user: current_user # class + the server's declared inputs
150
154
  end
151
155
  ```
152
156
 
@@ -170,7 +174,7 @@ result[:error] # => true when the tool reported a failure
170
174
  and raise at definition time.
171
175
  - The class form binds the tools of one instance built at definition time —
172
176
  for servers that act per-user, pass a ready instance instead
173
- (`mcp Linear.new(user: current_user)`) where the user is available.
177
+ (`mcps Linear.new(user: current_user)`) where the user is available.
174
178
 
175
179
  The sandbox itself never sees the network — every `SB.call` performs the MCP
176
180
  request on the host. A runnable example lives in `sample/ruby_llm_mcp/`.
@@ -5,14 +5,14 @@ require "security_box"
5
5
 
6
6
  module RubyLLM
7
7
  class CodeMode < Tool
8
- VERSION = "0.2.0"
8
+ VERSION = "0.3.0"
9
9
 
10
10
  DEFAULT_TIMEOUT_MS = 30_000
11
11
  DEFAULT_FUEL_MS = 10_000
12
12
  MAX_TOOLS = SecurityBox::Rpcs::MAX_RPCS
13
13
 
14
- TOOL_USAGE = "tool expects a RubyLLM::Tool class or instance, " \
15
- 'or exactly one "name" => tool pair'
14
+ TOOL_USAGE = "tools expects RubyLLM::Tool classes or instances, " \
15
+ 'or "name" => tool pairs'
16
16
 
17
17
  FIXED_DESCRIPTION = <<~DESC.freeze
18
18
  Executes Ruby code inside a secure sandbox and returns what it produced.
@@ -69,22 +69,38 @@ module RubyLLM
69
69
  add_mount(source, dest, :read_write, description)
70
70
  end
71
71
 
72
- def tool(mapping = nil, **kwargs)
73
- if !kwargs.empty?
74
- raise ArgumentError, TOOL_USAGE unless kwargs.size == 1 && mapping.nil?
75
-
76
- name, bound = kwargs.first
77
- mapping = { name.to_s => bound }
78
- end
79
-
80
- if mapping.is_a?(Hash)
81
- raise ArgumentError, TOOL_USAGE unless mapping.size == 1
82
-
83
- name, bound = mapping.first
84
- add_tool(bound, name.to_s)
85
- else
86
- add_tool(mapping, nil)
72
+ # Binds RubyLLM::Tool classes/instances as host tools callable from
73
+ # inside the sandbox via SB.call:
74
+ #
75
+ # tools Weather # class (instantiated with .new)
76
+ # tools Weather, Echo # several at once
77
+ # tools [Weather, Echo] # or arrays
78
+ # tools "forecast" => Weather # explicit sandbox name
79
+ # tools forecast: Weather # same, as kwargs
80
+ #
81
+ # Without arguments it returns the hash of bound tools. The name is
82
+ # derived from the class-name leaf (`MyApp::Tools::Weather` binds as
83
+ # `weather`). Fails fast: every item must be a RubyLLM::Tool (class or
84
+ # instance), names must be unique and non-empty, and if anything goes
85
+ # wrong nothing is bound.
86
+ def tools(*args, **kwargs)
87
+ return @tools ||= {} if args.empty? && kwargs.empty?
88
+
89
+ snapshot = @tools.dup
90
+ begin
91
+ args.flatten(1).each do |item|
92
+ if item.is_a?(Hash)
93
+ item.each { |name, bound| add_tool(bound, name.to_s) }
94
+ else
95
+ add_tool(item, nil)
96
+ end
97
+ end
98
+ kwargs.each { |name, bound| add_tool(bound, name.to_s) }
99
+ rescue StandardError
100
+ instance_variable_set(:@tools, snapshot)
101
+ raise
87
102
  end
103
+ tools
88
104
  end
89
105
 
90
106
  # Connects MCP server(s) and binds all their tools into the sandbox:
@@ -92,21 +108,24 @@ module RubyLLM
92
108
  # docs = RubyLLM.mcp(url: "https://learn.microsoft.com/api/mcp")
93
109
  #
94
110
  # class DocsResearch < RubyLLM::CodeMode
95
- # mcp docs # instance
96
- # mcp MicrosoftDocs # or the class (instantiated with .new)
97
- # mcp Linear, user: current_user # class + declared inputs
98
- # mcp Docs, Github # several servers / arrays at once
111
+ # mcps docs # instance
112
+ # mcps MicrosoftDocs # or the class (instantiated with .new)
113
+ # mcps Linear, user: current_user # class + declared inputs
114
+ # mcps Docs, Github # several servers / arrays at once
99
115
  # end
100
116
  #
101
- # Server tools keep their names (`microsoft_docs_search`, ...); a tool
102
- # whose name is already bound is re-bound as "<server>_tool". The
103
- # servers' instructions (when the server sends them) are snapshotted
104
- # at definition and listed in the description under "## Server notes".
117
+ # Without arguments it returns the array of connected servers. Server
118
+ # tools keep their names (`microsoft_docs_search`, ...); a tool whose
119
+ # name is already bound is re-bound as "<server>_tool". The servers'
120
+ # instructions (when the server sends them) are snapshotted at
121
+ # definition and listed in the description under "## Server notes".
105
122
  # Fails fast: every argument must be a RubyLLM::MCP (class or
106
123
  # instance), and if anything goes wrong — a server that cannot be
107
124
  # reached, a name that stays colliding after prefixing — nothing is
108
125
  # bound.
109
- def mcp(*servers, **inputs)
126
+ def mcps(*servers, **inputs)
127
+ return @mcps ||= [] if servers.empty? && inputs.empty?
128
+
110
129
  flat = servers.flatten(1)
111
130
  raise ArgumentError, "expected at least one RubyLLM::MCP server" if flat.empty?
112
131
  unless inputs.empty? || flat.all?(Class)
@@ -127,14 +146,6 @@ module RubyLLM
127
146
  @mounts ||= []
128
147
  end
129
148
 
130
- def tools
131
- @tools ||= {}
132
- end
133
-
134
- def mcps
135
- @mcps ||= []
136
- end
137
-
138
149
  def configuration
139
150
  @configuration ||= SecurityBox::Configuration.build(
140
151
  timeout_ms: DEFAULT_TIMEOUT_MS,
@@ -263,7 +274,7 @@ module RubyLLM
263
274
  def normalize_mcp_servers(servers, inputs)
264
275
  unless defined?(RubyLLM::MCP)
265
276
  raise ArgumentError,
266
- "RubyLLM::MCP is not available; mcp needs a ruby_llm version with MCP support"
277
+ "RubyLLM::MCP is not available; mcps needs a ruby_llm version with MCP support"
267
278
  end
268
279
 
269
280
  servers.map do |server|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-code_mode
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Juneira