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.



121
122
123
124
125
126
127
128
# File 'lib/brute/tools/adapter.rb', line 121

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.



94
95
96
97
98
99
100
101
102
103
# File 'lib/brute/tools/adapter.rb', line 94

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.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/brute/tools/adapter.rb', line 106

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

  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)


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

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

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

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 ::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

.wrap_all(tools) ⇒ Object

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



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

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.



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

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.



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/brute/tools/adapter.rb', line 143

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