Class: Phronomy::Tool::Base

Inherits:
RubyLLM::Tool
  • Object
show all
Defined in:
lib/phronomy/tool/base.rb

Overview

Base class extending RubyLLM::Tool with Phronomy-specific DSL.

Additional DSL over RubyLLM::Tool:

  • tool_name : explicit function name exposed to the LLM (overrides auto-conversion)
  • scope : access-scope metadata (:read_only, :write, etc.)
  • on_error : error-handling policy (:raise or :return_empty)
  • on_schema_error : behavior when LLM passes schema-violating arguments :return_error (default), :raise, or :coerce
  • requires_approval : require human approval before execution
  • param :name, enum: [...] : restrict allowed values in the JSON Schema

Examples:

class SearchKnowledgeBase < Phronomy::Tool::Base
  tool_name "search_kb"               # explicit name shown to the LLM
  description "Search the internal knowledge base"
  param :query,  type: :string, desc: "Search query"
  param :lang,   type: :string, desc: "Language", required: false, enum: %w[en ja fr]
  scope :read_only
  on_error :return_empty

  def execute(query:, lang: "en")
    KnowledgeBase.search(query, lang: lang)
  end
end

Direct Known Subclasses

AgentTool, McpTool

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

._sleep_proc#call

Injectable sleep callable for testing. Defaults to Kernel#sleep.

Returns:



130
131
132
# File 'lib/phronomy/tool/base.rb', line 130

def _sleep_proc
  @_sleep_proc || method(:sleep)
end

Class Method Details

.on_error(behavior = nil) ⇒ Object

Configures error-handling behavior.

Parameters:

  • behavior (Symbol) (defaults to: nil)

    :raise (default) or :return_empty



71
72
73
74
75
# File 'lib/phronomy/tool/base.rb', line 71

def on_error(behavior = nil)
  return @on_error || :raise if behavior.nil?

  @on_error = behavior
end

.on_schema_error(behavior = nil) ⇒ Object

Configures how this tool responds when the LLM passes arguments that violate the declared parameter types or enum constraints.

Parameters:

  • behavior (Symbol) (defaults to: nil)

    :return_error (default) — return a descriptive error string as the tool result so the LLM can self-correct on the next turn. :raise — raise Phronomy::ToolError, stopping the agent loop. :coerce — attempt type coercion (e.g. "42" → 42 for :integer); falls back to :return_error when coercion is not possible.



86
87
88
89
90
# File 'lib/phronomy/tool/base.rb', line 86

def on_schema_error(behavior = nil)
  return @on_schema_error || :return_error if behavior.nil?

  @on_schema_error = behavior
end

.param(name, enum: nil, **options) ⇒ Object

Extends RubyLLM::Tool.param with an optional +enum:+ keyword. The enum values are stored separately and injected into the JSON Schema produced by #params_schema.

Parameters:

  • name (Symbol)

    parameter name

  • enum (Array, nil) (defaults to: nil)

    allowed values; when given, added as "enum" in JSON Schema

  • options (Hash)

    forwarded to RubyLLM::Tool.param



49
50
51
52
# File 'lib/phronomy/tool/base.rb', line 49

def param(name, enum: nil, **options)
  super(name, **options)
  param_enums[name] = enum if enum
end

.param_enumsHash{Symbol => Array}

Returns the enum constraints registered via .param.

Returns:

  • (Hash{Symbol => Array})


56
57
58
# File 'lib/phronomy/tool/base.rb', line 56

def param_enums
  @param_enums ||= {}
end

.requires_approval(value = nil) ⇒ Object

Configures whether human approval is required before executing this tool.

Parameters:

  • value (Boolean) (defaults to: nil)


94
95
96
97
98
# File 'lib/phronomy/tool/base.rb', line 94

def requires_approval(value = nil)
  return @requires_approval || false if value.nil?

  @requires_approval = value
end

.retry_on(*exception_classes, times: 1, wait: 0, base: 1.0) ⇒ Object

Registers a retry policy for one or more exception classes.

When the tool raises one of the listed exception classes, it will be retried up to +times+ times with the specified wait strategy. Multiple policies can be registered and are evaluated in order.

GuardrailError is never retried regardless of this configuration.

Examples:

retry_on Phronomy::ToolError, times: 3, wait: :exponential, base: 1.0
retry_on Net::ReadTimeout, times: 2, wait: 0.5

Parameters:

  • exception_classes (Array<Class>)

    exception classes to retry on

  • times (Integer) (defaults to: 1)

    maximum retry attempts (default: 1)

  • wait (Symbol, Numeric) (defaults to: 0)

    :exponential, :linear, or a fixed Float

  • base (Float) (defaults to: 1.0)

    base wait time in seconds (default: 1.0)



116
117
118
119
# File 'lib/phronomy/tool/base.rb', line 116

def retry_on(*exception_classes, times: 1, wait: 0, base: 1.0)
  @retry_policies ||= []
  @retry_policies << {exceptions: exception_classes, times: times, wait: wait, base: base}
end

.retry_policiesArray<Hash>

Returns all retry policies registered on this tool class.

Returns:

  • (Array<Hash>)


123
124
125
# File 'lib/phronomy/tool/base.rb', line 123

def retry_policies
  @retry_policies || []
end

.scope(value = nil) ⇒ Object

Sets the access scope for this tool (metadata; enforcement is the responsibility of the Graph/Guardrail layer).

Parameters:

  • value (Symbol) (defaults to: nil)

    e.g. :read_only, :write, :admin



63
64
65
66
67
# File 'lib/phronomy/tool/base.rb', line 63

def scope(value = nil)
  return @scope if value.nil?

  @scope = value
end

.tool_name(value = nil) ⇒ Object

Sets an explicit function name to expose to the LLM, bypassing RubyLLM's automatic CamelCase-to-snake_case conversion. When omitted, RubyLLM's default conversion applies (e.g. WeatherTool → "weather").

Parameters:

  • value (String, nil) (defaults to: nil)

    the exact function name the LLM will see



36
37
38
39
40
# File 'lib/phronomy/tool/base.rb', line 36

def tool_name(value = nil)
  return @tool_name if value.nil?

  @tool_name = value.to_s
end

Instance Method Details

#call(args) ⇒ Object

Overrides RubyLLM::Tool#call to apply schema validation, the retry policy, the on_error policy, and wrap errors as ToolError.

Execution order:

  1. Schema validation (type + enum checks).
  2. Call super(validated_args) inside a retry loop.
  3. On persistent failure, apply on_error policy.


173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/phronomy/tool/base.rb', line 173

def call(args)
  validated_args, schema_error = validate_and_coerce(args)
  if schema_error
    case self.class.on_schema_error
    when :raise
      raise Phronomy::ToolError, "#{self.class.name} schema error: #{schema_error}"
    else
      # :return_error (default) and coerce fallback
      return "Schema validation failed: #{schema_error}"
    end
  end
  with_tool_retry { super(validated_args) }
rescue Phronomy::ToolError
  raise
rescue => e
  case self.class.on_error
  when :return_empty
    []
  else
    raise Phronomy::ToolError, "#{self.class.name} execution failed: #{e.message}"
  end
end

#execute(**_args) ⇒ String

This method is abstract.

Subclasses must implement this method.

Override this method to implement the tool's logic.

The method receives the declared param fields as keyword arguments. The return value is passed back to the LLM as the tool result.

Examples:

class WeatherTool < Phronomy::Tool::Base
  description "Get current weather"
  param :location, type: :string, desc: "City name"

  def execute(location:)
    WeatherService.fetch(location).to_s
  end
end

Returns:

  • (String)

    result string returned to the LLM

Raises:

  • (NotImplementedError)


217
218
219
# File 'lib/phronomy/tool/base.rb', line 217

def execute(**_args)
  raise NotImplementedError, "#{self.class}#execute is not implemented"
end

#nameObject

Returns the function name exposed to the LLM. Uses the class-level tool_name if set; otherwise falls back to RubyLLM's automatic conversion (CamelCase → snake_case, strips trailing "_tool").



142
143
144
# File 'lib/phronomy/tool/base.rb', line 142

def name
  self.class.tool_name || super
end

#params_schemaObject

Returns the JSON Schema for this tool's parameters. Injects "enum" entries for any param declared with enum: [...].



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/phronomy/tool/base.rb', line 148

def params_schema
  schema = super
  return schema if schema.nil? || self.class.param_enums.empty?

  enums = self.class.param_enums
  properties = schema.dig("properties") || schema.dig(:properties)
  return schema unless properties

  enums.each do |param_name, values|
    key = properties.key?(param_name.to_s) ? param_name.to_s : param_name.to_sym
    next unless properties[key]

    properties[key]["enum"] = values.map(&:to_s)
  end

  schema
end

#requires_approval?Boolean

Instance method for requires_approval? (convenience accessor).

Returns:

  • (Boolean)


197
198
199
# File 'lib/phronomy/tool/base.rb', line 197

def requires_approval?
  self.class.requires_approval
end