Class: Pikuri::Tool::Parameters

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/tool/parameters.rb

Overview

Schema for a Pikuri::Tool‘s arguments. Built up via the fluent <required|optional>_<type> methods, then frozen by Parameters.build; serializes to the OpenAI JSON-Schema shape via #to_h and validates LLM-supplied argument hashes via #validate.

Examples:

params = Tool::Parameters.build { |p| p.required_string :query, 'The query.' }
params.to_h
# => {type: 'object',
#     properties: {query: {type: 'string', description: 'The query.'}},
#     required: ['query']}
params.validate('query' => 'cats') # => {query: 'cats'}

Defined Under Namespace

Classes: ValidationError

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeParameters



38
39
40
41
# File 'lib/pikuri/tool/parameters.rb', line 38

def initialize
  @properties = {}
  @required = []
end

Class Method Details

.build {|builder| ... } ⇒ Parameters

Yield a fresh builder, freeze it, and return it.

Yield Parameters:

Returns:

  • (Parameters)

    frozen builder, safe to share between calls



31
32
33
34
35
# File 'lib/pikuri/tool/parameters.rb', line 31

def self.build
  builder = new
  yield builder
  builder.freeze
end

Instance Method Details

#freezeself

Freeze the builder along with its internal collections, so post-build mutation attempts raise FrozenError instead of silently succeeding.

Returns:

  • (self)


47
48
49
50
51
# File 'lib/pikuri/tool/parameters.rb', line 47

def freeze
  @properties.freeze
  @required.freeze
  super
end

#optional_boolean(name, description) ⇒ self

Add an optional boolean property. See #required_boolean for accepted shapes.

Parameters:

  • name (Symbol)

    property name

  • description (String)

    human-readable description shown to the LLM

Returns:

  • (self)


162
163
164
# File 'lib/pikuri/tool/parameters.rb', line 162

def optional_boolean(name, description)
  add(name, 'boolean', description, required: false)
end

#optional_enum(name, description, values:) ⇒ self

Add an optional enum property. See #required_enum for the values contract and validation behavior.

Parameters:

  • name (Symbol)

    property name

  • description (String)

    human-readable description shown to the LLM

  • values (Array<String>)

    non-empty list of allowed values

Returns:

  • (self)

Raises:

  • (ArgumentError)

    if values is not a non-empty Array of non-empty Strings



194
195
196
# File 'lib/pikuri/tool/parameters.rb', line 194

def optional_enum(name, description, values:)
  add_enum(name, description, values, required: false)
end

#optional_integer(name, description) ⇒ self

Add an optional integer property. See #required_integer for accepted shapes.

Parameters:

  • name (Symbol)

    property name

  • description (String)

    human-readable description shown to the LLM

Returns:

  • (self)


118
119
120
# File 'lib/pikuri/tool/parameters.rb', line 118

def optional_integer(name, description)
  add(name, 'integer', description, required: false)
end

#optional_number(name, description) ⇒ self

Add an optional number property. See #required_number for accepted shapes.

Parameters:

  • name (Symbol)

    property name

  • description (String)

    human-readable description shown to the LLM

Returns:

  • (self)


139
140
141
# File 'lib/pikuri/tool/parameters.rb', line 139

def optional_number(name, description)
  add(name, 'number', description, required: false)
end

#optional_string(name, description) ⇒ self

Add an optional string property.

Parameters:

  • name (Symbol)

    property name

  • description (String)

    human-readable description shown to the LLM

Returns:

  • (self)


67
68
69
# File 'lib/pikuri/tool/parameters.rb', line 67

def optional_string(name, description)
  add(name, 'string', description, required: false)
end

#required_boolean(name, description) ⇒ self

Add a required boolean property. Accepts Ruby true/false as-is, and the literal Strings “true”/“false” (some models surface JSON booleans as Strings) after trimming surrounding whitespace. Other Strings, numbers, and nil are rejected —there is no truthy-coercion of “yes” / 0 / etc.

Parameters:

  • name (Symbol)

    property name

  • description (String)

    human-readable description shown to the LLM

Returns:

  • (self)


152
153
154
# File 'lib/pikuri/tool/parameters.rb', line 152

def required_boolean(name, description)
  add(name, 'boolean', description, required: true)
end

#required_enum(name, description, values:) ⇒ self

Add a required enum property — a string field constrained to one of a fixed set of values. Emits JSON-Schema enum alongside type: ‘string’, which the LLM treats as a closed choice. Validation rejects any string outside the set with an LLM-actionable error message listing the allowed values.

Parameters:

  • name (Symbol)

    property name

  • description (String)

    human-readable description shown to the LLM

  • values (Array<String>)

    non-empty list of allowed values; each entry must be a non-empty String. The list is dup‘d and frozen at insertion so callers can’t mutate it later.

Returns:

  • (self)

Raises:

  • (ArgumentError)

    if values is not a non-empty Array of non-empty Strings (build-time check — surfaces as a host-side bug rather than an LLM-facing validation error)



181
182
183
# File 'lib/pikuri/tool/parameters.rb', line 181

def required_enum(name, description, values:)
  add_enum(name, description, values, required: true)
end

#required_integer(name, description) ⇒ self

Add a required integer property. Accepts Integers, Floats with a zero fractional part (e.g. 1.0), and base-10 numeric Strings (after trimming) that resolve to whole numbers; rejects everything else.

Parameters:

  • name (Symbol)

    property name

  • description (String)

    human-readable description shown to the LLM

Returns:

  • (self)


108
109
110
# File 'lib/pikuri/tool/parameters.rb', line 108

def required_integer(name, description)
  add(name, 'integer', description, required: true)
end

#required_number(name, description) ⇒ self

Add a required number property (JSON-Schema number: Integer or finite Float). Numeric Strings (after trimming) are parsed; NaN and Infinity are rejected.

Parameters:

  • name (Symbol)

    property name

  • description (String)

    human-readable description shown to the LLM

Returns:

  • (self)


129
130
131
# File 'lib/pikuri/tool/parameters.rb', line 129

def required_number(name, description)
  add(name, 'number', description, required: true)
end

#required_string(name, description) ⇒ self

Add a required string property.

Parameters:

  • name (Symbol)

    property name

  • description (String)

    human-readable description shown to the LLM

Returns:

  • (self)


58
59
60
# File 'lib/pikuri/tool/parameters.rb', line 58

def required_string(name, description)
  add(name, 'string', description, required: true)
end

#required_string_array(name, description) ⇒ self

Add a required array-of-string property — JSON-Schema {type: ‘array’, items: {type: ‘string’}}. The LLM sends a native JSON array in the tool-call arguments (the shape its training data overwhelmingly uses for list-valued parameters), so there is no in-band encoding for it to get wrong. The value must arrive as an Array — no JSON-encoded-array-in-a-string fallback. Element coercion mirrors the scalar fields’ one documented leniency, in reverse: Integers and finite Floats are converted to their to_s form (a model emitting [“Fix issue 12”, 42] meant a string list — the conversion is unambiguous), while booleans, nil, and nested structures are rejected — those signal a genuinely wrong call shape, not a representational quirk. An empty array is type-valid; rejecting it (if the tool needs at least one element) is the tool’s job, with a tool-specific error message.

Parameters:

  • name (Symbol)

    property name

  • description (String)

    human-readable description shown to the LLM

Returns:

  • (self)


91
92
93
94
95
96
97
98
99
# File 'lib/pikuri/tool/parameters.rb', line 91

def required_string_array(name, description)
  @properties[name] = {
    type: 'array',
    items: { type: 'string' },
    description: description
  }
  @required << name.to_s
  self
end

#to_hHash

Schema in OpenAI JSON-Schema shape.

Returns:

  • (Hash)

    {type: ‘object’, properties: {…}, required: […]}



201
202
203
# File 'lib/pikuri/tool/parameters.rb', line 201

def to_h
  { type: 'object', properties: @properties, required: @required }
end

#validate(args) ⇒ Hash{Symbol=>Object}

Validate a tool-call argument hash against the declared schema. Returns a symbol-keyed hash safe to splat as kwargs into a tool’s execute Proc; raises ValidationError with an LLM-actionable message listing every missing/unknown/mistyped field and reprinting the schema.

Strict: unknown keys are rejected (with DidYouMean suggestions), wrong types are rejected. All issues are collected and reported together so the LLM can fix them in one round trip.

Parameters:

  • args (Hash)

    arguments as decoded from the tool-call JSON; keys may be Strings or Symbols

Returns:

  • (Hash{Symbol=>Object})

    validated, symbol-keyed arguments

Raises:

  • (ValidationError)

    if args is not a Hash, contains unknown keys, omits a required key, or has a value of the wrong type



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/pikuri/tool/parameters.rb', line 219

def validate(args)
  raise ValidationError, "Arguments must be an object, got #{args.class}." unless args.is_a?(Hash)

  symbolized = args.transform_keys(&:to_sym)
  errors = []
  result = {}

  (symbolized.keys - @properties.keys).each do |unknown|
    errors << unknown_key_error(unknown)
  end

  @properties.each do |name, schema|
    if symbolized.key?(name)
      begin
        coerced = coerce(symbolized[name], schema[:type])
        raise CoercionError, enum_message(schema[:enum], coerced) if schema[:enum] && !schema[:enum].include?(coerced)

        result[name] = coerced
      rescue CoercionError => e
        errors << "Parameter `#{name}` #{e.message}."
      end
    elsif @required.include?(name.to_s)
      errors << missing_required_message(name, schema)
    end
  end

  return result if errors.empty?

  raise ValidationError, build_error_message(errors)
end