Class: Brute::Tools::Adapter
- Inherits:
-
Object
- Object
- Brute::Tools::Adapter
- 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:
-
Using any tools library — anything that quacks like a tool (RubyLLM::Tool today, others via their own adapters) is wrapped into the same interface.
-
Avoiding tool libraries entirely — Brute::Turn::ToolPipeline and Tools::SubAgent work without inheriting from a library class.
-
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
-
#description ⇒ Object
readonly
Returns the value of attribute description.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#original ⇒ Object
readonly
The tool object this adapter wraps (RubyLLM::Tool, Brute::Turn::ToolPipeline, SubAgent, Hash definition, ...).
-
#params ⇒ Object
readonly
Returns the value of attribute params.
Class Method Summary collapse
-
.from_duck_type(tool) ⇒ Object
Anything tool-shaped: needs #name and #call or #execute.
-
.from_hash(definition) ⇒ Object
Quick inline tool: { name:, description:, params:, execute: }.
-
.from_ruby_llm(tool) ⇒ Object
A RubyLLM::Tool instance (the library's own arg normalization and validation stays in play via tool.call).
-
.wrap(tool) ⇒ Object
Wrap a single tool of any supported shape.
-
.wrap_all(tools) ⇒ Object
Wrap a list of tools into a { name_sym => adapter } lookup hash — the shape ToolPipeline works with.
Instance Method Summary collapse
-
#call(arguments = {}) ⇒ Object
Execute the tool.
-
#initialize(name:, description:, params:, handler:, schema: nil, original: nil) ⇒ Adapter
constructor
A new instance of Adapter.
-
#to_h ⇒ Object
Library-neutral tool definition (JSON-Schema-ish), for completion middlewares that talk to an HTTP API directly.
-
#to_ruby_llm ⇒ Object
Convert to a RubyLLM::Tool so ruby_llm-backed completion can hand the tool to its providers.
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
#description ⇒ Object (readonly)
Returns the value of attribute description.
39 40 41 |
# File 'lib/brute/tools/adapter.rb', line 39 def description @description end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
39 40 41 |
# File 'lib/brute/tools/adapter.rb', line 39 def name @name end |
#original ⇒ Object (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 |
#params ⇒ Object (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: }
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_h ⇒ Object
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_llm ⇒ Object
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 |