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

Constant Summary collapse

EMPTY =

Returns:

new.freeze

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)


50
51
52
53
54
# File 'lib/pikuri/tool/parameters.rb', line 50

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)


156
157
158
# File 'lib/pikuri/tool/parameters.rb', line 156

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



188
189
190
# File 'lib/pikuri/tool/parameters.rb', line 188

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)


112
113
114
# File 'lib/pikuri/tool/parameters.rb', line 112

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)


133
134
135
# File 'lib/pikuri/tool/parameters.rb', line 133

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)


70
71
72
# File 'lib/pikuri/tool/parameters.rb', line 70

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)


146
147
148
# File 'lib/pikuri/tool/parameters.rb', line 146

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)



175
176
177
# File 'lib/pikuri/tool/parameters.rb', line 175

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)


102
103
104
# File 'lib/pikuri/tool/parameters.rb', line 102

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)


123
124
125
# File 'lib/pikuri/tool/parameters.rb', line 123

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)


61
62
63
# File 'lib/pikuri/tool/parameters.rb', line 61

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 (+'array', items: {type: 'string'}+). The value must arrive as a native JSON Array — no JSON-in-a-string fallback. Element coercion mirrors the scalars in reverse: Integers and finite Floats become their to_s (a model emitting ["Fix issue 12", 42] meant a string list), while booleans, nil, and nested structures are rejected. An empty array is type-valid — rejecting it is the tool's job.

Parameters:

  • name (Symbol)

    property name

  • description (String)

    human-readable description shown to the LLM

Returns:

  • (self)


85
86
87
88
89
90
91
92
93
# File 'lib/pikuri/tool/parameters.rb', line 85

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

#to_openaiHash

Schema in OpenAI JSON-Schema shape.

Returns:

  • (Hash)

    {type: 'object', properties: {...}, required: [...]}



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

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

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

Validate a tool-call argument hash against the schema. Returns a symbol-keyed hash safe to splat as kwargs into a tool's execute; raises ValidationError with an LLM-actionable message listing every missing/unknown/mistyped field and reprinting the schema. Strict: unknown keys rejected (with DidYouMean suggestions), wrong types rejected, all issues collected so the LLM fixes them in one round trip.

Parameters:

  • args (Hash)

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

Returns:

  • (Hash{Symbol=>Object})

    validated, symbol-keyed arguments

Raises:

  • (ValidationError)

    if args isn't a Hash, has unknown keys, omits a required key, or has a wrong-typed value



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/pikuri/tool/parameters.rb', line 211

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