Class: ActiveAgent::Providers::Common::BaseModel

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::Attributes, ActiveModel::Model
Defined in:
lib/active_agent/providers/common/model.rb

Overview

BaseModel provides a foundation for structured data models with compressed serialization support.

This class extends ActiveModel functionality to provide:

  • Attribute definition with default values

  • Compressed hash serialization (excludes default values)

  • Required attribute tracking

  • Deep compaction of nested structures

Example

class Message < BaseModel
  attribute :role, :string, as: "user"
  attribute :type, :string, fallback: "plain/text"
  attribute :content, :string
end

message = Message.new(content: "Hello")
message.to_h      #=> { role: "user", type: "plain/text", content: "Hello" }
message.serialize #=> { role: "user", type: "plain/text", content: "Hello" }

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(kwargs = {}) ⇒ BaseModel

Initializes a new instance with the given attributes.

Attributes can be provided as a hash. Hash keys are sorted to prioritize nested objects during initialization for backwards compatibility. A special internal key ‘__default_values` can be passed to get an instance with only default values without any overrides.

Examples:

Message.new(role: "user", content: "Hello")

Parameters:

  • kwargs (Hash) (defaults to: {})

    attributes to initialize the instance with



161
162
163
164
165
166
167
168
169
170
# File 'lib/active_agent/providers/common/model.rb', line 161

def initialize(kwargs = {})
  # To allow us to get a list of attribute defaults without initialized overrides
  return super(nil) if kwargs.key?(:'__default_values')

  # Backwards Compatibility: This sorts object construction to the top to protect the assignment
  #   of backward compatibility assignments.
  kwargs = kwargs.sort_by { |k, v| v.is_a?(Hash) ? 0 : 1 }.to_h if kwargs.is_a?(Hash)

  super(kwargs)
end

Class Method Details

.attribute(name, type = nil, **options) ⇒ Object

Defines an attribute on the model with special handling for default values.

Examples:

Define a read-only attribute with a default value

attribute :role, :string, as: "user"

Define an attribute with a fallback value

attribute :temperature, :float, fallback: 0.7

Define a regular attribute

attribute :messages, :array

Parameters:

  • name (Symbol, String)

    the name of the attribute

  • type (Class, nil) (defaults to: nil)

    the type of the attribute (optional)

  • options (Hash)

    additional options for the attribute

Options Hash (**options):

  • :as (Object)

    A default value that makes the attribute read-only. When set, attempts to assign a different value will raise an ArgumentError. This attribute will be included in the compressed hash representation.

  • :fallback (Object)

    A default value for the attribute. This attribute will be included in the compressed hash representation.

Raises:

  • (ArgumentError)

    if attempting to set a value different from the :as default



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/active_agent/providers/common/model.rb', line 69

def self.attribute(name, type = nil, **options)
  if options.key?(:as)
    default_value = options.delete(:as)
    super(name, type, default: default_value, **options)

    # Track this attribute as required (must be included in compressed hash)
    required_attributes << name.to_s

    define_method("#{name}=") do |value|
      normalized_value   = value.is_a?(String)         ? value.to_sym         : value
      normalized_default = default_value.is_a?(String) ? default_value.to_sym : default_value

      next if normalized_value == normalized_default

      raise ArgumentError, "Cannot set '#{name}' attribute (read-only with default value)"
    end
  elsif options.key?(:fallback)
    default_value = options.delete(:fallback)
    super(name, type, default: default_value, **options)

    # Track this attribute as required (must be included in compressed hash)
    required_attributes << name.to_s
  else
    super(name, type, **options)
  end
end

.delegate_attributes(*attributes, to:) ⇒ Object

Delegates attribute accessors to another object.

Creates getter and setter methods that forward to the specified target object. If the target is nil when setting, an empty hash is initialized.

Examples:

delegate_attributes :temperature, :max_tokens, to: :options

Parameters:

  • attributes (Array<Symbol>)

    attribute names to delegate

  • to (Symbol)

    the target method/attribute name



106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/active_agent/providers/common/model.rb', line 106

def self.delegate_attributes(*attributes, to:)
  attributes.each do |attribute|
    define_method(attribute) do
      public_send(to)&.public_send(attribute)
    end

    define_method("#{attribute}=") do |value|
      public_send("#{to}=", {}) if public_send(to).nil?

      public_send(to).public_send("#{attribute}=", value)
    end
  end
end

.drop_attributes(*attributes) ⇒ void

This method returns an undefined value.

Drops specified attributes by defining no-op setters.

This is useful when converting between providers that support different attributes or when dropping attributes during message response to request construction. The attributes can still be read if defined elsewhere, but setting them has no effect.

Examples:

drop_attributes :metadata, :extra_info

Parameters:

  • attributes (Array<Symbol>)

    attribute names to drop



131
132
133
134
135
136
137
# File 'lib/active_agent/providers/common/model.rb', line 131

def self.drop_attributes(*attributes)
  attributes.each do |attribute|
    define_method("#{attribute}=") do |value|
      # No-Op: Drop the value
    end
  end
end

.inherited(subclass) ⇒ void

This method returns an undefined value.

Ensures subclasses get their own required_attributes set.

Parameters:

  • subclass (Class)

    the inheriting class



43
44
45
46
# File 'lib/active_agent/providers/common/model.rb', line 43

def self.inherited(subclass)
  super
  subclass.instance_variable_set(:@required_attributes, required_attributes.dup)
end

.keysArray<Symbol>

Returns all attribute keys including aliases.

Combines both the main attribute type keys and any attribute aliases, ensuring all possible attribute names are represented as symbols.

Returns:

  • (Array<Symbol>)

    array of all attribute keys



145
146
147
# File 'lib/active_agent/providers/common/model.rb', line 145

def self.keys
  (attribute_types.keys.map(&:to_sym) | attribute_aliases.keys.map(&:to_sym))
end

.required_attributesSet<String>

Returns the set of required attribute names that must be included in compressed output.

Required attributes are those defined with the as option, which establishes a default value that should always be serialized.

Returns:

  • (Set<String>)

    set of required attribute names



35
36
37
# File 'lib/active_agent/providers/common/model.rb', line 35

def self.required_attributes
  @required_attributes ||= Set.new
end

Instance Method Details

#<=>(other) ⇒ Integer?

Compares two models based on their serialized representations.

Uses the serialized hash to compare models, allowing for sorting and equality comparisons based on attribute values rather than object identity.

Examples:

model1 = Message.new(content: "A")
model2 = Message.new(content: "B")
model1 <=> model2  #=> -1

Parameters:

  • other (BaseModel)

    the model to compare against

Returns:

  • (Integer, nil)

    -1, 0, 1, or nil if not comparable



339
340
341
# File 'lib/active_agent/providers/common/model.rb', line 339

def <=>(other)
  serialize <=> other&.serialize
end

#==(other) ⇒ Boolean

Compares equality based on serialized representation.

Two models are equal if their serialized hashes are equal, regardless of object identity. This allows value-based equality comparisons.

Examples:

model1 = Message.new(content: "Hello")
model2 = Message.new(content: "Hello")
model1 == model2  #=> true

Parameters:

  • other (BaseModel)

    the model to compare against

Returns:

  • (Boolean)


355
356
357
# File 'lib/active_agent/providers/common/model.rb', line 355

def ==(other)
  serialize == other&.serialize
end

#deep_compact(kwargs = {}) ⇒ Hash

Recursively removes nil values and empty collections from a hash.

Nested hashes and arrays are processed recursively. Empty hashes and arrays after compaction are also removed.

Examples:

deep_compact({ a: 1, b: nil, c: { d: nil, e: 2 } })
#=> { a: 1, c: { e: 2 } }

Parameters:

  • kwargs (Hash) (defaults to: {})

    hash to compact

Returns:

  • (Hash)

    compacted hash with nil values and empty collections removed



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/active_agent/providers/common/model.rb', line 198

def deep_compact(kwargs = {})
  kwargs.each_with_object({}) do |(key, value), result|
    compacted_value = case value
    when Hash
      deep_compacted = deep_compact(value)
      deep_compacted unless deep_compacted.empty?
    when Array
      compacted_array = value.map { |v| v.is_a?(Hash) ? deep_compact(v) : v }.compact
      compacted_array unless compacted_array.empty?
    else
      value
    end

    result[key] = compacted_value unless compacted_value.nil?
  end
end

#deep_dupBaseModel

Creates a deep duplicate of the model.

Duplicates the model instance and recursively duplicates any array or hash attributes to ensure complete independence from the original object.

Returns:

  • (BaseModel)

    deep duplicate of the model



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/active_agent/providers/common/model.rb', line 250

def deep_dup
  dup.tap do |duplicated|
    attribute_names.each do |name|
      value = public_send(name)
      next if value.nil?

      duplicated.public_send("#{name}=", case value
      when Array
        value.map { |v| v.respond_to?(:deep_dup) ? v.deep_dup : v.dup rescue v }
      when Hash
        value.deep_dup
      when BaseModel
        value.deep_dup
      else
        value.dup rescue value
      end)
    end
  end
end

#inspectString Also known as: to_s

Returns a string representation for inspection.

Provides a readable view of the model showing the class name and non-default attributes in a format similar to standard Ruby object inspection.

Examples:

message = Message.new(role: "user", content: "Hello")
message.inspect
#=> "#<Message role: \"user\", content: \"Hello\">"

Returns:

  • (String)

    formatted string representation

See Also:



311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/active_agent/providers/common/model.rb', line 311

def inspect
  attrs = JSON.pretty_generate(serialize, {
    space: " ",
    indent: "  ",
    object_nl: "\n",
    array_nl: "\n"
  }).lines.drop(1).join.chomp.sub(/\}\z/, "").strip

  return "#<#{self.class.name}>" if attrs.empty?

  "#<#{self.class.name} {\n  #{attrs}\n}>"
end

#merge!(kwargs = {}) ⇒ BaseModel

Merges the given attributes into the current instance.

Only attributes with corresponding setter methods are updated. Keys are symbolized before merging.

Parameters:

  • kwargs (Hash) (defaults to: {})

    attribute keyword arguments to merge

Returns:



179
180
181
182
183
184
185
# File 'lib/active_agent/providers/common/model.rb', line 179

def merge!(kwargs = {})
  kwargs.deep_symbolize_keys.each do |key, value|
    public_send("#{key}=", value) if respond_to?("#{key}=")
  end

  self
end

#serializeHash

Serializes the model using attribute type serializers.

Iterates through each attribute and uses its ActiveModel::Type serializer to convert the value to its serialized form. Only non-default values are included, except for required attributes (those defined with ‘:as` or `:fallback` options). This provides a compressed serialization that respects custom type logic.

Examples:

message = Message.new(role: "user", content: "Hello")
message.serialize  #=> { role: "user", content: "Hello" }

Returns:

  • (Hash)

    serialized representation with non-default and required attributes



282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/active_agent/providers/common/model.rb', line 282

def serialize
  default_values = self.class.new(__default_values: true).attributes
  required_attrs = self.class.required_attributes

  deep_compact(attribute_names.each_with_object({}) do |name, hash|
    value = public_send(name)

    # Always include required attributes (those defined with 'as' option)
    # or attributes that differ from their default value
    next if value == default_values[name] && !required_attrs.include?(name)

    # Use the attribute's type serializer
    attr_type = self.class.attribute_types[name]
    hash[name.to_sym] = attr_type.serialize(value)
  end)
end

#to_hHash

Alias for #to_hash.

Returns:

  • (Hash)

    hash representation of all attributes

See Also:



242
# File 'lib/active_agent/providers/common/model.rb', line 242

def to_h = to_hash

#to_hashHash

Converts the model to a hash representation.

Recursively converts nested BaseModel instances and arrays to hashes. Nil values and empty collections are removed via deep_compact.

Examples:

message.to_hash
#=> { role: "user", content: "Hello", metadata: { id: 1 } }

Returns:

  • (Hash)

    hash representation of all attributes



225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/active_agent/providers/common/model.rb', line 225

def to_hash
  deep_compact(attribute_names.each_with_object({}) do |name, hash|
    value = public_send(name)

    hash[name.to_sym] = case value
    when BaseModel then value.to_hash
    when Array     then value.map { _1.is_a?(BaseModel) ? _1.to_hash : _1 }
    else
      value
    end
  end)
end