Class: Mistri::Schema

Inherits:
Object
  • Object
show all
Defined in:
lib/mistri/schema.rb

Overview

A small builder for tool argument schemas, so a tool declares its inputs in Ruby instead of hand-writing JSON Schema. It emits the object schema every provider accepts:

Schema.build do
string :city, "City name", required: true
string :units, "Temperature units", enum: %w[celsius fahrenheit]
integer :days, "Forecast length"
end

A raw JSON Schema hash is always accepted directly for anything the builder does not cover, so the DSL is a convenience, never a ceiling.

Constant Summary collapse

TYPES =
{
  "object" => ->(v) { v.is_a?(Hash) }, "array" => ->(v) { v.is_a?(Array) },
  "string" => ->(v) { v.is_a?(String) }, "integer" => ->(v) { v.is_a?(Integer) },
  "number" => ->(v) { v.is_a?(Numeric) }, "boolean" => ->(v) { [true, false].include?(v) },
  "null" => lambda(&:nil?)
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeSchema

Returns a new instance of Schema.



25
26
27
28
# File 'lib/mistri/schema.rb', line 25

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

Class Method Details

.buildObject

instance_exec, not instance_eval: it binds self to the builder without passing an argument, so a zero-arity lambda works as naturally as a proc.



19
20
21
22
23
# File 'lib/mistri/schema.rb', line 19

def self.build(&)
  builder = new
  builder.instance_exec(&)
  builder.to_h
end

.strict(schema, all_required: false) ⇒ Object

Prepares a schema for constrained decoding: every object gains additionalProperties: false (Anthropic and OpenAI both demand it), and all_required marks every property required (OpenAI strict mode's rule). String keys throughout, ready for the wire.



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/mistri/schema.rb', line 89

def strict(schema, all_required: false)
  spec = schema.transform_keys(&:to_s)
  out = spec.dup
  if spec["type"] == "object" || spec.key?("properties")
    props = (spec["properties"] || {}).to_h do |key, member|
      [key.to_s, strict(member, all_required: all_required)]
    end
    out["properties"] = props
    out["additionalProperties"] = false
    out["required"] = all_required ? props.keys : Array(spec["required"]).map(&:to_s)
  end
  if spec["items"].is_a?(Hash)
    out["items"] =
      strict(spec["items"], all_required: all_required)
  end
  out
end

.violations(value, schema, path = "$") ⇒ Object

Violations of a value against the schema subset the harness emits and providers constrain: types (including type arrays), object properties with required and additionalProperties: false, array items, enum. Empty means valid; entries are human-readable, written to be fed back to a model for one-shot correction.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/mistri/schema.rb', line 69

def violations(value, schema, path = "$")
  spec = schema.transform_keys(&:to_s)
  mismatch = type_violation(value, spec, path)
  return [mismatch] if mismatch

  errors = []
  if (enum = spec["enum"]) && !enum.include?(value)
    errors << "#{path} must be one of: #{enum.join(", ")}"
  end
  case value
  when Hash then errors.concat(object_violations(value, spec, path))
  when Array then errors.concat(array_violations(value, spec, path))
  end
  errors
end

Instance Method Details

#array(name, description = nil, items: { type: "string" }, required: false, **extra) ⇒ Object



41
42
43
44
45
46
47
# File 'lib/mistri/schema.rb', line 41

def array(name, description = nil, items: { type: "string" }, required: false, **extra)
  prop = { type: "array", items: items }
  prop[:description] = description if description
  @properties[name.to_s] = prop.merge(extra)
  @required << name.to_s if required
  nil
end

#object(name, description = nil, required: false) ⇒ Object



49
50
51
52
53
54
55
# File 'lib/mistri/schema.rb', line 49

def object(name, description = nil, required: false, &)
  prop = self.class.build(&)
  prop[:description] = description if description
  @properties[name.to_s] = prop
  @required << name.to_s if required
  nil
end

#to_hObject



57
58
59
60
61
# File 'lib/mistri/schema.rb', line 57

def to_h
  schema = { type: "object", properties: @properties }
  schema[:required] = @required unless @required.empty?
  schema
end