Class: Toolchest::Toolbox

Inherits:
Object
  • Object
show all
Includes:
AbstractController::Callbacks, ActiveSupport::Callbacks, ActiveSupport::Rescuable
Defined in:
lib/toolchest/toolbox.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(params: {}, tool_definition: nil) ⇒ Toolbox

Returns a new instance of Toolbox.



174
175
176
177
178
179
# File 'lib/toolchest/toolbox.rb', line 174

def initialize(params: {}, tool_definition: nil)
  @params = Parameters.new(params, tool_definition: tool_definition)
  @_tool_definition = tool_definition
  @_response = nil
  @_suggests = []
end

Instance Attribute Details

#paramsObject (readonly)

Returns the value of attribute params.



172
173
174
# File 'lib/toolchest/toolbox.rb', line 172

def params
  @params
end

Class Method Details

.controller_nameObject



155
# File 'lib/toolchest/toolbox.rb', line 155

def controller_name = name&.underscore&.chomp("_toolbox") || "anonymous"

.default_param(name, type, description = "", **options) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/toolchest/toolbox.rb', line 57

def default_param(name, type, description = "", **options)
  @_default_params << {
    param: ParamDefinition.new(
      name: name, type: type, description: description,
      optional: options.fetch(:optional, false),
      enum: options[:enum],
      default: options.fetch(:default, :__unset__)
    ),
    except: Array(options[:except]).map(&:to_sym),
    only: options[:only] ? Array(options[:only]).map(&:to_sym) : nil
  }
end

.default_paramsObject



30
31
32
33
34
35
# File 'lib/toolchest/toolbox.rb', line 30

def default_params
  ancestors
    .select { |a| a.respond_to?(:own_default_params, true) }
    .reverse
    .flat_map { |a| a.send(:own_default_params) }
end

.helper(*modules) ⇒ Object

Include modules as view helpers.

helper ApplicationHelper
helper FormattingHelper, CurrencyHelper


135
136
137
# File 'lib/toolchest/toolbox.rb', line 135

def helper(*modules)
  @_helper_modules.concat(modules)
end

.helper_method(*methods) ⇒ Object

Expose toolbox methods as view helpers, like controller helper_method.

helper_method :current_user, :admin?


126
127
128
# File 'lib/toolchest/toolbox.rb', line 126

def helper_method(*methods)
  @_helper_methods.concat(methods.map(&:to_sym))
end

.helper_methodsObject



139
140
141
142
143
144
145
# File 'lib/toolchest/toolbox.rb', line 139

def helper_methods
  ancestors
    .select { |a| a.respond_to?(:own_helper_methods, true) }
    .reverse
    .flat_map { |a| a.send(:own_helper_methods) }
    .uniq
end

.helper_modulesObject



147
148
149
150
151
152
153
# File 'lib/toolchest/toolbox.rb', line 147

def helper_modules
  ancestors
    .select { |a| a.respond_to?(:own_helper_modules, true) }
    .reverse
    .flat_map { |a| a.send(:own_helper_modules) }
    .uniq
end

.inherited(subclass) ⇒ Object



12
13
14
15
16
17
18
19
20
21
# File 'lib/toolchest/toolbox.rb', line 12

def inherited(subclass)
  super
  subclass.instance_variable_set(:@_tool_definitions, {})
  subclass.instance_variable_set(:@_default_params, [])
  subclass.instance_variable_set(:@_resources, [])
  subclass.instance_variable_set(:@_prompts, [])
  subclass.instance_variable_set(:@_pending_tool, nil)
  subclass.instance_variable_set(:@_helper_methods, [])
  subclass.instance_variable_set(:@_helper_modules, [])
end

.method_added(method_name) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/toolchest/toolbox.rb', line 92

def method_added(method_name)
  super
  return unless @_pending_tool

  pending = @_pending_tool
  @_pending_tool = nil

  params = pending[:builder].params.dup

  default_params.each do |dp|
    next if dp[:except].include?(method_name.to_sym)
    next if dp[:only] && !dp[:only].include?(method_name.to_sym)
    next if params.any? { |p| p.name == dp[:param].name }
    params.unshift(dp[:param])
  end

  definition = ToolDefinition.new(
    method_name: method_name,
    description: pending[:description],
    params: params,
    toolbox_class: self,
    custom_name: pending[:custom_name],
    access_level: pending[:access_level],
    scope: pending[:scope],
    annotations: pending[:annotations]
  )

  @_tool_definitions[method_name.to_sym] = definition
end

.prompt(prompt_name, description: nil, arguments: {}, &block) ⇒ Object



82
83
84
85
86
87
88
89
90
# File 'lib/toolchest/toolbox.rb', line 82

def prompt(prompt_name, description: nil, arguments: {}, &block)
  @_prompts << {
    name: prompt_name,
    description: description,
    arguments: arguments,
    block: block,
    toolbox_class: self
  }
end

.promptsObject



44
45
46
47
48
49
# File 'lib/toolchest/toolbox.rb', line 44

def prompts
  ancestors
    .select { |a| a.respond_to?(:own_prompts, true) }
    .reverse
    .flat_map { |a| a.send(:own_prompts) }
end

.resource(uri, name: nil, description: nil, &block) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
# File 'lib/toolchest/toolbox.rb', line 70

def resource(uri, name: nil, description: nil, &block)
  template = uri.include?("{")
  @_resources << {
    uri: uri,
    name: name || uri,
    description: description,
    block: block,
    template: template,
    toolbox_class: self
  }
end

.resourcesObject



37
38
39
40
41
42
# File 'lib/toolchest/toolbox.rb', line 37

def resources
  ancestors
    .select { |a| a.respond_to?(:own_resources, true) }
    .reverse
    .flat_map { |a| a.send(:own_resources) }
end

.tool(description, name: nil, access: nil, scope: nil, annotations: nil, &block) ⇒ Object



51
52
53
54
55
# File 'lib/toolchest/toolbox.rb', line 51

def tool(description, name: nil, access: nil, scope: nil, annotations: nil, &block)
  builder = ToolBuilder.new
  builder.instance_eval(&block) if block
  @_pending_tool = { description:, custom_name: name, access_level: access, scope:, annotations:, builder: }
end

.tool_definitionsObject



23
24
25
26
27
28
# File 'lib/toolchest/toolbox.rb', line 23

def tool_definitions
  ancestors
    .select { |a| a.respond_to?(:own_tool_definitions, true) }
    .reverse
    .each_with_object({}) { |a, h| h.merge!(a.send(:own_tool_definitions)) }
end

Instance Method Details

#action_nameObject



185
# File 'lib/toolchest/toolbox.rb', line 185

def action_name = @_action_name.to_s

#authObject



181
# File 'lib/toolchest/toolbox.rb', line 181

def auth = Toolchest::Current.auth

#controller_nameObject



183
# File 'lib/toolchest/toolbox.rb', line 183

def controller_name = self.class.controller_name

#dispatch(action_name) ⇒ Object



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/toolchest/toolbox.rb', line 304

def dispatch(action_name)
  @_action_name = action_name

  catch(:halt) do
    begin
      process_action(action_name)
    rescue => e
      unless rescue_with_handler(e)
        raise
      end
    end
  end

  implicit_render! unless performed?
  build_mcp_response
end

#halt(**response) ⇒ Object



225
226
227
228
229
230
# File 'lib/toolchest/toolbox.rb', line 225

def halt(**response)
  if response[:error]
    render_error(response[:error])
  end
  throw :halt
end

#mcp_log(level, message) ⇒ Object



232
# File 'lib/toolchest/toolbox.rb', line 232

def mcp_log(level, message) = Toolchest.router(Toolchest::Current.mount_key&.to_sym || :default).notify_log(level: level.to_s, message: message)

#mcp_progress(progress, total: nil, message: nil) ⇒ Object

Report progress during long-running actions. Client shows a progress bar. total and message are optional.



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/toolchest/toolbox.rb', line 236

def mcp_progress(progress, total: nil, message: nil)
  session = Toolchest::Current.mcp_session
  return unless session

  token = Toolchest::Current.mcp_progress_token
  return unless token

  session.notify_progress(
    progress_token: token,
    progress: progress,
    total: total,
    message: message,
    related_request_id: Toolchest::Current.mcp_request_id
  )
end

#mcp_sample(prompt = nil, context: nil, max_tokens: 1024, **kwargs, &block) ⇒ Object

Ask the client’s LLM to do work. Returns the response text.

mcp_sample("Summarize this order", context: @order.to_json)

mcp_sample do |s|
  s.system "You are a fraud analyst"
  s.user "Analyze: #{@order.to_json}"
  s.max_tokens 500
  s.temperature 0.3
end

Raises:



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/toolchest/toolbox.rb', line 262

def mcp_sample(prompt = nil, context: nil, max_tokens: 1024, **kwargs, &block)
  session = Toolchest::Current.mcp_session
  raise Toolchest::Error, "Sampling requires an MCP client that supports it" unless session

  if block
    builder = SamplingBuilder.new
    yield builder
    messages = builder.messages
    options = { max_tokens: builder.max_tokens_value || max_tokens }
    options[:system_prompt] = builder.system_value if builder.system_value
    options[:temperature] = builder.temperature_value if builder.temperature_value
    options[:model_preferences] = builder.model_preferences_value if builder.model_preferences_value
    options[:stop_sequences] = builder.stop_sequences_value if builder.stop_sequences_value
  else
    text = prompt.to_s
    text = "#{text}\n\n#{context}" if context
    messages = [{ role: "user", content: { type: "text", text: text } }]
    options = { max_tokens: max_tokens }
    options[:system_prompt] = kwargs[:system] if kwargs[:system]
    options[:temperature] = kwargs[:temperature] if kwargs[:temperature]
  end

  begin
    result = session.create_sampling_message(
      messages: messages,
      related_request_id: Toolchest::Current.mcp_request_id,
      **options
    )
  rescue RuntimeError => e
    raise Toolchest::Error, "Sampling failed: #{e.message}"
  end

  # Extract text from response
  content = result[:content] || result["content"]
  case content
  when Hash then content[:text] || content["text"]
  when Array then content.map { |c| c[:text] || c["text"] }.compact.join("\n")
  when String then content
  else result.to_s
  end
end

#performed?Boolean

Returns:

  • (Boolean)


187
# File 'lib/toolchest/toolbox.rb', line 187

def performed? = @_response.present?

#render(action_or_template = nil, json: nil, text: nil) ⇒ Object



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/toolchest/toolbox.rb', line 189

def render(action_or_template = nil, json: nil, text: nil)
  result = if json
    json.is_a?(String) ? json : json.to_json
  elsif text
    text
  else
    rendered = Renderer.render(self, action_or_template || action_name)
    rendered.is_a?(String) ? rendered : rendered.to_json
  end

  @_response = {
    content: [{ type: "text", text: result }],
    isError: false
  }
end

#render_error(message) ⇒ Object



205
206
207
208
209
210
# File 'lib/toolchest/toolbox.rb', line 205

def render_error(message)
  @_response = {
    content: [{ type: "text", text: message }],
    isError: true
  }
end

#render_errors(record) ⇒ Object



212
213
214
215
# File 'lib/toolchest/toolbox.rb', line 212

def render_errors(record)
  messages = record.errors.full_messages.join(", ")
  render_error("Validation failed: #{messages}")
end

#suggests(tool_name, hint = nil) ⇒ Object



217
218
219
220
221
222
223
# File 'lib/toolchest/toolbox.rb', line 217

def suggests(tool_name, hint = nil)
  tool_name = tool_name.to_s
  if tool_name.exclude?("_") && tool_name.exclude?(".") && tool_name.exclude?("/")
    tool_name = Naming.generate(self.class, tool_name)
  end
  @_suggests << { tool: tool_name, hint: hint }
end