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 (#name plus #call or #execute) is wrapped into the same interface.

  2. Avoiding tool libraries entirely — Brute::Tool, 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

The inline run proc converts adapters (via #to_h) into whatever its LLM library expects; 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.



134
135
136
137
138
139
140
141
# File 'lib/brute/tools/adapter.rb', line 134

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.



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

def description
  @description
end

#nameObject (readonly)

Returns the value of attribute name.



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

def name
  @name
end

#originalObject (readonly)

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



132
133
134
# File 'lib/brute/tools/adapter.rb', line 132

def original
  @original
end

#paramsObject (readonly)

Returns the value of attribute params.



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

def params
  @params
end

Class Method Details

.from_brute_tool(tool) ⇒ Object

A Brute::Tool instance. Tools declared with the params(...) schema DSL keep their full JSON schema.



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

def self.from_brute_tool(tool)
  new(
    name:        tool.name.to_s,
    description: tool.description,
    params:      tool.params,
    schema:      tool.params_schema,
    handler:     ->(**args) { tool.call(args) },
    original:    tool,
  )
end

.from_duck_type(tool) ⇒ Object

Anything tool-shaped: needs #name and #call or #execute.



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

def self.from_duck_type(tool)
  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

  if tool.respond_to?(:execute)
    entry = tool.method(:execute)
  else
    entry = tool.method(:call)
  end
  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: }



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/brute/tools/adapter.rb', line 81

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

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

.wrap(tool) ⇒ Object

Wrap a single tool of any supported shape. Idempotent.



41
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
68
69
# File 'lib/brute/tools/adapter.rb', line 41

def self.wrap(tool)
  if tool.is_a?(Adapter)
    tool
  else
    if tool.is_a?(Class)
      tool = tool.new
    end
    case tool
    when Hash                then from_hash(tool)
    when ::Brute::Tool       then from_brute_tool(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
end

.wrap_all(tools) ⇒ Object

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



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

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). The inline run proc reshapes this into whatever its LLM library expects.



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/brute/tools/adapter.rb', line 152

def to_h
  if @schema
    { name: @name, description: @description, parameters: @schema.deep_symbolize_keys }
  else
    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
end