Class: Brute::Tools::Adapter

Inherits:
Object
  • Object
show all
Defined in:
lib/brute/tools/adapter.rb

Overview

Normalizes any tool shape into one neutral interface so the rest of Brute never has to care which tools library (if any) a tool was written with.

This solves three problems:

  1. Using any tools library — anything that quacks like a tool (RubyLLM::Tool today, others via their own adapters) is wrapped into the same interface.

  2. Avoiding tool libraries entirely — Brute::Turn::ToolPipeline and Tools::SubAgent work without inheriting from a library class.

  3. Quickly adding tools — a plain Hash with a proc is enough:

    Brute::Tools::Adapter.wrap(
    name:        "echo",
    description: "Echo the input back",
    params:      { msg: { type: "string", required: true } },
    execute:     ->(msg:) { msg },
    )
    

The neutral interface:

adapter.name        # String
adapter.description # String
adapter.params      # { key => { type:, desc:, required: } }
adapter.call(args)  # execute with a (string- or symbol-keyed) Hash

Completion middlewares convert adapters into whatever their LLM library expects (e.g. #to_ruby_llm); ToolPipeline executes them via #call.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, description:, params:, handler:, schema: nil, original: nil) ⇒ Adapter

Returns a new instance of Adapter.



130
131
132
133
134
135
136
137
# File 'lib/brute/tools/adapter.rb', line 130

def initialize(name:, description:, params:, handler:, schema: nil, original: nil)
  @name        = name
  @description = description
  @params      = params || {}
  @schema      = schema
  @handler     = handler
  @original    = original
end

Instance Attribute Details

#descriptionObject (readonly)

Returns the value of attribute description.



39
40
41
# File 'lib/brute/tools/adapter.rb', line 39

def description
  @description
end

#nameObject (readonly)

Returns the value of attribute name.



39
40
41
# File 'lib/brute/tools/adapter.rb', line 39

def name
  @name
end

#originalObject (readonly)

The tool object this adapter wraps (RubyLLM::Tool, Brute::Turn::ToolPipeline, SubAgent, Hash definition, ...).



141
142
143
# File 'lib/brute/tools/adapter.rb', line 141

def original
  @original
end

#paramsObject (readonly)

Returns the value of attribute params.



39
40
41
# File 'lib/brute/tools/adapter.rb', line 39

def params
  @params
end

Class Method Details

.from_duck_type(tool) ⇒ Object

Anything tool-shaped: needs #name and #call or #execute. Honors #to_ruby_llm for backward compatibility with existing adapters.



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/brute/tools/adapter.rb', line 113

def self.from_duck_type(tool)
  return from_ruby_llm(tool.to_ruby_llm) if tool.respond_to?(:to_ruby_llm)

  unless tool.respond_to?(:name) && (tool.respond_to?(:call) || tool.respond_to?(:execute))
    raise ArgumentError, "don't know how to adapt #{tool.inspect} into a tool"
  end

  entry = tool.respond_to?(:execute) ? tool.method(:execute) : tool.method(:call)
  new(
    name:        tool.name.to_s,
    description: tool.respond_to?(:description) ? tool.description : "",
    params:      tool.respond_to?(:params) ? tool.params : {},
    handler:     ->(**args) { entry.call(**args) },
    original:    tool,
  )
end

.from_hash(definition) ⇒ Object

Quick inline tool: { name:, description:, params:, execute: }

Raises:

  • (ArgumentError)


79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/brute/tools/adapter.rb', line 79

def self.from_hash(definition)
  definition = definition.transform_keys(&:to_sym)
  handler = definition.fetch(:execute) { definition[:handler] }
  raise ArgumentError, "inline tool needs an :execute proc" unless handler.respond_to?(:call)

  new(
    name:        definition.fetch(:name).to_s,
    description: definition.fetch(:description, ""),
    params:      definition.fetch(:params, {}),
    handler:     ->(**args) { handler.call(**args) },
    original:    definition,
  )
end

.from_ruby_llm(tool) ⇒ Object

A RubyLLM::Tool instance (the library's own arg normalization and validation stays in play via tool.call). Tools declared with the params(...) schema DSL keep their full JSON schema.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/brute/tools/adapter.rb', line 96

def self.from_ruby_llm(tool)
  params = tool.parameters.each_with_object({}) do |(key, param), hash|
    hash[key.to_sym] = { type: param.type, desc: param.description, required: param.required }.compact
  end

  new(
    name:        tool.name.to_s,
    description: tool.description,
    params:      params,
    schema:      (tool.params_schema if tool.respond_to?(:params_schema)),
    handler:     ->(**args) { tool.call(args) },
    original:    tool,
  )
end

.wrap(tool) ⇒ Object

Wrap a single tool of any supported shape. Idempotent.



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/brute/tools/adapter.rb', line 42

def self.wrap(tool)
  return tool if tool.is_a?(Adapter)

  tool = tool.new if tool.is_a?(Class)

  case tool
  when Hash                then from_hash(tool)
  when ::RubyLLM::Tool     then from_ruby_llm(tool)
  when Brute::Tools::SubAgent then new(
    name:        tool.name,
    description: tool.description,
    params:      tool.params,
    handler:     ->(**args) { tool.execute(args) },
    original:    tool,
  )
  when Brute::Turn::ToolPipeline then new(
    name:        tool.name,
    description: tool.description,
    params:      tool.params,
    handler:     ->(**args) { tool.call(**args) },
    original:    tool,
  )
  else
    from_duck_type(tool)
  end
end

.wrap_all(tools) ⇒ Object

Wrap a list of tools into a { name_sym => adapter } lookup hash — the shape ToolPipeline works with.



71
72
73
74
75
76
# File 'lib/brute/tools/adapter.rb', line 71

def self.wrap_all(tools)
  Array(tools).each_with_object({}) do |tool, hash|
    adapter = wrap(tool)
    hash[adapter.name.to_sym] = adapter
  end
end

Instance Method Details

#call(arguments = {}) ⇒ Object

Execute the tool. Accepts string- or symbol-keyed argument hashes, as delivered by LLM providers.



145
146
147
148
# File 'lib/brute/tools/adapter.rb', line 145

def call(arguments = {})
  args = arguments.to_h.transform_keys(&:to_sym)
  @handler.call(**args)
end

#to_hObject

Library-neutral tool definition (JSON-Schema-ish), for completion middlewares that talk to an HTTP API directly.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/brute/tools/adapter.rb', line 167

def to_h
  return { name: @name, description: @description, parameters: @schema.deep_symbolize_keys } if @schema

  properties = @params.transform_values do |opts|
    {
      type:        opts[:type] || "string",
      description: opts[:desc] || opts[:description],
      items:       opts[:items],
      enum:        opts[:enum],
    }.compact
  end
  required = @params.select { |_k, opts| opts[:required] }.keys

  {
    name:        @name,
    description: @description,
    parameters: {
      type:       "object",
      properties: properties,
      required:   required.map(&:to_s),
    },
  }
end

#to_ruby_llmObject

Convert to a RubyLLM::Tool so ruby_llm-backed completion can hand the tool to its providers. Returns the wrapped tool untouched when it already is one.



153
154
155
156
157
158
159
160
161
162
163
# File 'lib/brute/tools/adapter.rb', line 153

def to_ruby_llm
  return @original if @original.is_a?(::RubyLLM::Tool)

  adapter = self
  Class.new(::RubyLLM::Tool) do
    description adapter.description
    adapter.params.each { |key, opts| param key, **opts.slice(:type, :desc, :required) }
    define_method(:name) { adapter.name }
    define_method(:execute) { |**args| adapter.call(args) }
  end.new
end