Class: ActiveAgent::Delegation::Schema

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

Overview

Declarative JSON Schema for a delegated agent's inputs and outputs.

A delegation is only as good as its contract: the calling model needs to know exactly what a sub-agent accepts, and the calling agent needs to know what shape comes back. Schema builds both from a small DSL, from a plain JSON Schema hash, or from any class that responds to to_json_schema (see SchemaGenerator).

Examples:

DSL

Schema.build do
  string  :text, required: true, description: "Document to summarize"
  integer :max_points, description: "How many bullets to return"
  array   :tags, of: :string, description: "Topic tags"
end

Plain JSON Schema

Schema.build(type: "object", properties: { text: { type: "string" } }, required: [ "text" ])

ActiveModel / ActiveRecord

Schema.build(ContactForm) # ContactForm includes ActiveAgent::SchemaGenerator

Constant Summary collapse

SCALAR_TYPES =

Scalar types that get a one-line DSL helper.

%i[string integer number boolean].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeSchema

Returns a new instance of Schema.



72
73
74
75
76
# File 'lib/active_agent/delegation/schema.rb', line 72

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

Class Method Details

.build(source = nil) { ... } ⇒ Schema

Coerces any supported schema source into a Schema.

Parameters:

  • source (Schema, Hash, Class, nil) (defaults to: nil)

    existing schema, raw JSON Schema, or a class responding to to_json_schema

Yields:

  • DSL block (evaluated against a new Schema, or yielded when arity is 1)

Returns:

Raises:

  • (ArgumentError)

    when the source cannot be interpreted



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/active_agent/delegation/schema.rb', line 37

def build(source = nil, &block)
  schema =
    case source
    when Schema then source
    when Hash   then from_hash(source)
    when nil    then new
    else
      if source.respond_to?(:to_json_schema)
        from_hash(source.to_json_schema)
      else
        raise ArgumentError, "Cannot build a delegation schema from #{source.inspect}. " \
          "Pass a Hash, a class responding to #to_json_schema, or use the block DSL."
      end
    end

  if block
    block.arity == 1 ? block.call(schema) : schema.instance_eval(&block)
  end

  schema
end

.from_hash(hash) ⇒ Schema

Parameters:

  • hash (Hash)

    JSON Schema object

Returns:



61
62
63
64
65
66
67
68
69
# File 'lib/active_agent/delegation/schema.rb', line 61

def from_hash(hash)
  hash = hash.deep_symbolize_keys

  new.tap do |schema|
    (hash[:properties] || {}).each { |name, definition| schema.property(name, definition) }
    schema.required(*Array(hash[:required]))
    schema.additional_properties(hash.fetch(:additionalProperties, hash.fetch(:additional_properties, false)))
  end
end

Instance Method Details

#additional_properties(value = true) ⇒ Object

Parameters:

  • value (Boolean) (defaults to: true)


157
158
159
# File 'lib/active_agent/delegation/schema.rb', line 157

def additional_properties(value = true)
  @additional_properties = value
end

#array(name, of: nil, **options) { ... } ⇒ Object

Declares an array property.

Parameters:

  • name (Symbol, String)
  • of (Symbol, String, Hash, nil) (defaults to: nil)

    item type, or a JSON Schema fragment for items

Yields:

  • nested DSL describing an object item type



134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/active_agent/delegation/schema.rb', line 134

def array(name, of: nil, **options, &block)
  items =
    if block
      self.class.build(&block).to_json_schema
    elsif of.is_a?(Hash)
      of.deep_symbolize_keys
    elsif of
      { type: of.to_s }
    end

  param(name, :array, **options.merge({ items: items }.compact))
end

#empty?Boolean

Returns true when nothing has been declared.

Returns:

  • (Boolean)

    true when nothing has been declared



79
80
81
# File 'lib/active_agent/delegation/schema.rb', line 79

def empty?
  @properties.empty?
end

#keysArray<Symbol>

Returns declared property names.

Returns:

  • (Array<Symbol>)

    declared property names



162
163
164
# File 'lib/active_agent/delegation/schema.rb', line 162

def keys
  @properties.keys
end

#missing_keys(payload) ⇒ Array<Symbol>

Validates a parsed payload against the declared required properties.

This is a deliberately shallow check: it catches the failure that actually happens in practice (a model omitting a required key) without pulling a full JSON Schema validator into the gem's dependencies.

Parameters:

  • payload (Hash, nil)

Returns:

  • (Array<Symbol>)

    missing required keys



211
212
213
214
215
216
217
# File 'lib/active_agent/delegation/schema.rb', line 211

def missing_keys(payload)
  return required_keys if payload.nil?
  return [] unless payload.is_a?(Hash)

  keys = payload.keys.map { |key| key.to_s.underscore.to_sym }
  required_keys.reject { |key| keys.include?(key) }
end

#object(name, **options) { ... } ⇒ Object

Declares an object property.

Parameters:

  • name (Symbol, String)

Yields:

  • nested DSL



125
126
127
# File 'lib/active_agent/delegation/schema.rb', line 125

def object(name, **options, &block)
  param(name, :object, **options, &block)
end

#param(name, type = :string, required: false, description: nil, **options) { ... } ⇒ void

This method returns an undefined value.

Declares a property.

Parameters:

  • name (Symbol, String)
  • type (Symbol, String) (defaults to: :string)

    JSON Schema type

  • required (Boolean) (defaults to: false)

    whether the calling model must supply it

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

    shown to the calling model — write it for a reader who has never seen your code

  • options (Hash)

    any other JSON Schema keyword (+enum+, format, minimum, ...)

Yields:

  • nested DSL for object properties



102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/active_agent/delegation/schema.rb', line 102

def param(name, type = :string, required: false, description: nil, **options, &block)
  definition = { type: type.to_s, description: description }.compact.merge(options)

  if block
    nested = self.class.build(&block)
    definition = definition.merge(nested.to_json_schema.except(:type))
    definition[:type] = "object"
  end

  property(name, definition)
  self.required(name) if required
end

#property(name, definition) ⇒ void

This method returns an undefined value.

Declares a property from an already-built JSON Schema fragment.

Parameters:

  • name (Symbol, String)
  • definition (Hash)

    JSON Schema fragment (e.g. { type: "string" })



88
89
90
# File 'lib/active_agent/delegation/schema.rb', line 88

def property(name, definition)
  @properties[name.to_sym] = definition.deep_symbolize_keys
end

#required(*names) ⇒ Array<Symbol>

Marks properties as required.

Parameters:

  • names (Array<Symbol, String>)

Returns:

  • (Array<Symbol>)


151
152
153
154
# File 'lib/active_agent/delegation/schema.rb', line 151

def required(*names)
  names.flatten.each { |name| @required |= [ name.to_sym ] }
  @required
end

#required_keysArray<Symbol>

Returns required property names.

Returns:

  • (Array<Symbol>)

    required property names



167
168
169
# File 'lib/active_agent/delegation/schema.rb', line 167

def required_keys
  @required.dup
end

#to_json_schemaHash Also known as: to_h

Note:

Deep-duplicated: providers normalize tool definitions in place, and a schema is shared by every generation that exposes it.

Returns JSON Schema object.

Returns:

  • (Hash)

    JSON Schema object



175
176
177
178
179
180
181
182
# File 'lib/active_agent/delegation/schema.rb', line 175

def to_json_schema
  {
    type: "object",
    properties: @properties.deep_dup,
    required: @required.map(&:to_s),
    additionalProperties: @additional_properties
  }
end

#to_response_format(name:, strict: true) ⇒ Hash

JSON Schema for a response_format payload.

ActiveAgent camelizes response-format schema keys on the way to the provider, but the required array holds string values — so they are camelized here to keep the emitted schema internally consistent. Assistant#parsed_json underscores the keys again on the way back, so agent code only ever sees snake_case.

Parameters:

  • name (String)

    schema name reported to the provider

  • strict (Boolean) (defaults to: true)

Returns:

  • (Hash)


196
197
198
199
200
201
# File 'lib/active_agent/delegation/schema.rb', line 196

def to_response_format(name:, strict: true)
  schema = to_json_schema
  schema[:required] = schema[:required].map { |key| key.camelize(:lower) }

  { name: name, schema: schema, strict: strict }
end