activeagent 0.6.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +240 -2
  3. data/README.md +15 -24
  4. data/lib/active_agent/base.rb +389 -39
  5. data/lib/active_agent/concerns/callbacks.rb +251 -0
  6. data/lib/active_agent/concerns/observers.rb +147 -0
  7. data/lib/active_agent/concerns/parameterized.rb +292 -0
  8. data/lib/active_agent/concerns/provider.rb +120 -0
  9. data/lib/active_agent/concerns/queueing.rb +36 -0
  10. data/lib/active_agent/concerns/rescue.rb +64 -0
  11. data/lib/active_agent/concerns/streaming.rb +282 -0
  12. data/lib/active_agent/concerns/tooling.rb +23 -0
  13. data/lib/active_agent/concerns/view.rb +150 -0
  14. data/lib/active_agent/configuration.rb +442 -20
  15. data/lib/active_agent/generation.rb +141 -47
  16. data/lib/active_agent/providers/_base_provider.rb +420 -0
  17. data/lib/active_agent/providers/anthropic/_types.rb +63 -0
  18. data/lib/active_agent/providers/anthropic/options.rb +53 -0
  19. data/lib/active_agent/providers/anthropic/request.rb +163 -0
  20. data/lib/active_agent/providers/anthropic/transforms.rb +353 -0
  21. data/lib/active_agent/providers/anthropic_provider.rb +254 -0
  22. data/lib/active_agent/providers/common/messages/_types.rb +160 -0
  23. data/lib/active_agent/providers/common/messages/assistant.rb +57 -0
  24. data/lib/active_agent/providers/common/messages/base.rb +17 -0
  25. data/lib/active_agent/providers/common/messages/system.rb +20 -0
  26. data/lib/active_agent/providers/common/messages/tool.rb +21 -0
  27. data/lib/active_agent/providers/common/messages/user.rb +20 -0
  28. data/lib/active_agent/providers/common/model.rb +361 -0
  29. data/lib/active_agent/providers/common/response.rb +13 -0
  30. data/lib/active_agent/providers/common/responses/_types.rb +51 -0
  31. data/lib/active_agent/providers/common/responses/base.rb +199 -0
  32. data/lib/active_agent/providers/common/responses/embed.rb +33 -0
  33. data/lib/active_agent/providers/common/responses/format.rb +31 -0
  34. data/lib/active_agent/providers/common/responses/message.rb +3 -0
  35. data/lib/active_agent/providers/common/responses/prompt.rb +42 -0
  36. data/lib/active_agent/providers/common/usage.rb +385 -0
  37. data/lib/active_agent/providers/concerns/exception_handler.rb +72 -0
  38. data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
  39. data/lib/active_agent/providers/concerns/previewable.rb +150 -0
  40. data/lib/active_agent/providers/log_subscriber.rb +178 -0
  41. data/lib/active_agent/providers/mock/_types.rb +77 -0
  42. data/lib/active_agent/providers/mock/embedding_request.rb +17 -0
  43. data/lib/active_agent/providers/mock/messages/_types.rb +103 -0
  44. data/lib/active_agent/providers/mock/messages/assistant.rb +26 -0
  45. data/lib/active_agent/providers/mock/messages/base.rb +63 -0
  46. data/lib/active_agent/providers/mock/messages/user.rb +18 -0
  47. data/lib/active_agent/providers/mock/options.rb +30 -0
  48. data/lib/active_agent/providers/mock/request.rb +38 -0
  49. data/lib/active_agent/providers/mock_provider.rb +311 -0
  50. data/lib/active_agent/providers/ollama/_types.rb +5 -0
  51. data/lib/active_agent/providers/ollama/chat/_types.rb +44 -0
  52. data/lib/active_agent/providers/ollama/chat/request.rb +249 -0
  53. data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
  54. data/lib/active_agent/providers/ollama/embedding/_types.rb +44 -0
  55. data/lib/active_agent/providers/ollama/embedding/request.rb +190 -0
  56. data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
  57. data/lib/active_agent/providers/ollama/options.rb +27 -0
  58. data/lib/active_agent/providers/ollama_provider.rb +94 -0
  59. data/lib/active_agent/providers/open_ai/_base.rb +59 -0
  60. data/lib/active_agent/providers/open_ai/_types.rb +5 -0
  61. data/lib/active_agent/providers/open_ai/chat/_types.rb +56 -0
  62. data/lib/active_agent/providers/open_ai/chat/request.rb +161 -0
  63. data/lib/active_agent/providers/open_ai/chat/transforms.rb +364 -0
  64. data/lib/active_agent/providers/open_ai/chat_provider.rb +219 -0
  65. data/lib/active_agent/providers/open_ai/embedding/_types.rb +56 -0
  66. data/lib/active_agent/providers/open_ai/embedding/request.rb +53 -0
  67. data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
  68. data/lib/active_agent/providers/open_ai/options.rb +74 -0
  69. data/lib/active_agent/providers/open_ai/responses/_types.rb +44 -0
  70. data/lib/active_agent/providers/open_ai/responses/request.rb +129 -0
  71. data/lib/active_agent/providers/open_ai/responses/transforms.rb +228 -0
  72. data/lib/active_agent/providers/open_ai/responses_provider.rb +200 -0
  73. data/lib/active_agent/providers/open_ai_provider.rb +94 -0
  74. data/lib/active_agent/providers/open_router/_types.rb +71 -0
  75. data/lib/active_agent/providers/open_router/options.rb +141 -0
  76. data/lib/active_agent/providers/open_router/request.rb +249 -0
  77. data/lib/active_agent/providers/open_router/requests/_types.rb +197 -0
  78. data/lib/active_agent/providers/open_router/requests/messages/_types.rb +56 -0
  79. data/lib/active_agent/providers/open_router/requests/messages/content/_types.rb +97 -0
  80. data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +43 -0
  81. data/lib/active_agent/providers/open_router/requests/messages/content/files/_types.rb +61 -0
  82. data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +37 -0
  83. data/lib/active_agent/providers/open_router/requests/plugin.rb +41 -0
  84. data/lib/active_agent/providers/open_router/requests/plugins/_types.rb +46 -0
  85. data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +51 -0
  86. data/lib/active_agent/providers/open_router/requests/prediction.rb +34 -0
  87. data/lib/active_agent/providers/open_router/requests/provider_preferences/_types.rb +44 -0
  88. data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +64 -0
  89. data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +105 -0
  90. data/lib/active_agent/providers/open_router/requests/response_format.rb +77 -0
  91. data/lib/active_agent/providers/open_router/transforms.rb +134 -0
  92. data/lib/active_agent/providers/open_router_provider.rb +62 -0
  93. data/lib/active_agent/providers/openai_provider.rb +2 -0
  94. data/lib/active_agent/providers/openrouter_provider.rb +2 -0
  95. data/lib/active_agent/railtie.rb +8 -6
  96. data/lib/active_agent/schema_generator.rb +333 -166
  97. data/lib/active_agent/version.rb +1 -1
  98. data/lib/active_agent.rb +112 -36
  99. data/lib/generators/active_agent/agent/USAGE +78 -0
  100. data/lib/generators/active_agent/{agent_generator.rb → agent/agent_generator.rb} +14 -4
  101. data/lib/generators/active_agent/install/USAGE +25 -0
  102. data/lib/generators/active_agent/{install_generator.rb → install/install_generator.rb} +1 -19
  103. data/lib/generators/active_agent/templates/agent.rb.tt +7 -3
  104. data/lib/generators/active_agent/templates/application_agent.rb.tt +0 -2
  105. data/lib/generators/erb/agent_generator.rb +31 -16
  106. data/lib/generators/erb/templates/instructions.md.erb.tt +3 -0
  107. data/lib/generators/erb/templates/instructions.md.tt +3 -0
  108. data/lib/generators/erb/templates/instructions.text.tt +1 -0
  109. data/lib/generators/erb/templates/message.md.erb.tt +5 -0
  110. data/lib/generators/erb/templates/schema.json.tt +10 -0
  111. data/lib/generators/test_unit/agent_generator.rb +1 -1
  112. data/lib/generators/test_unit/templates/functional_test.rb.tt +4 -2
  113. metadata +182 -71
  114. data/lib/active_agent/action_prompt/action.rb +0 -13
  115. data/lib/active_agent/action_prompt/base.rb +0 -623
  116. data/lib/active_agent/action_prompt/message.rb +0 -126
  117. data/lib/active_agent/action_prompt/prompt.rb +0 -136
  118. data/lib/active_agent/action_prompt.rb +0 -19
  119. data/lib/active_agent/callbacks.rb +0 -33
  120. data/lib/active_agent/generation_provider/anthropic_provider.rb +0 -163
  121. data/lib/active_agent/generation_provider/base.rb +0 -55
  122. data/lib/active_agent/generation_provider/base_adapter.rb +0 -19
  123. data/lib/active_agent/generation_provider/error_handling.rb +0 -167
  124. data/lib/active_agent/generation_provider/log_subscriber.rb +0 -92
  125. data/lib/active_agent/generation_provider/message_formatting.rb +0 -107
  126. data/lib/active_agent/generation_provider/ollama_provider.rb +0 -66
  127. data/lib/active_agent/generation_provider/open_ai_provider.rb +0 -279
  128. data/lib/active_agent/generation_provider/open_router_provider.rb +0 -385
  129. data/lib/active_agent/generation_provider/parameter_builder.rb +0 -119
  130. data/lib/active_agent/generation_provider/response.rb +0 -75
  131. data/lib/active_agent/generation_provider/responses_adapter.rb +0 -44
  132. data/lib/active_agent/generation_provider/stream_processing.rb +0 -58
  133. data/lib/active_agent/generation_provider/tool_management.rb +0 -142
  134. data/lib/active_agent/generation_provider.rb +0 -67
  135. data/lib/active_agent/log_subscriber.rb +0 -44
  136. data/lib/active_agent/parameterized.rb +0 -75
  137. data/lib/active_agent/prompt_helper.rb +0 -19
  138. data/lib/active_agent/queued_generation.rb +0 -12
  139. data/lib/active_agent/rescuable.rb +0 -34
  140. data/lib/active_agent/sanitizers.rb +0 -40
  141. data/lib/active_agent/streaming.rb +0 -34
  142. data/lib/active_agent/test_case.rb +0 -125
  143. data/lib/generators/USAGE +0 -47
  144. data/lib/generators/active_agent/USAGE +0 -56
  145. data/lib/generators/erb/install_generator.rb +0 -44
  146. data/lib/generators/erb/templates/layout.html.erb.tt +0 -1
  147. data/lib/generators/erb/templates/layout.json.erb.tt +0 -1
  148. data/lib/generators/erb/templates/layout.text.erb.tt +0 -1
  149. data/lib/generators/erb/templates/view.html.erb.tt +0 -5
  150. data/lib/generators/erb/templates/view.json.erb.tt +0 -16
  151. /data/lib/active_agent/{preview.rb → concerns/preview.rb} +0 -0
  152. /data/lib/generators/erb/templates/{view.text.erb.tt → message.text.erb.tt} +0 -0
@@ -0,0 +1,361 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Providers
5
+ module Common
6
+ # BaseModel provides a foundation for structured data models with compressed serialization support.
7
+ #
8
+ # This class extends ActiveModel functionality to provide:
9
+ # - Attribute definition with default values
10
+ # - Compressed hash serialization (excludes default values)
11
+ # - Required attribute tracking
12
+ # - Deep compaction of nested structures
13
+ #
14
+ # == Example
15
+ #
16
+ # class Message < BaseModel
17
+ # attribute :role, :string, as: "user"
18
+ # attribute :type, :string, fallback: "plain/text"
19
+ # attribute :content, :string
20
+ # end
21
+ #
22
+ # message = Message.new(content: "Hello")
23
+ # message.to_h #=> { role: "user", type: "plain/text", content: "Hello" }
24
+ # message.serialize #=> { role: "user", type: "plain/text", content: "Hello" }
25
+ class BaseModel
26
+ include ActiveModel::Model
27
+ include ActiveModel::Attributes
28
+
29
+ # Returns the set of required attribute names that must be included in compressed output.
30
+ #
31
+ # Required attributes are those defined with the +as+ option, which establishes
32
+ # a default value that should always be serialized.
33
+ #
34
+ # @return [Set<String>] set of required attribute names
35
+ def self.required_attributes
36
+ @required_attributes ||= Set.new
37
+ end
38
+
39
+ # Ensures subclasses get their own required_attributes set.
40
+ #
41
+ # @param subclass [Class] the inheriting class
42
+ # @return [void]
43
+ def self.inherited(subclass)
44
+ super
45
+ subclass.instance_variable_set(:@required_attributes, required_attributes.dup)
46
+ end
47
+
48
+ # Defines an attribute on the model with special handling for default values.
49
+ #
50
+ # @param name [Symbol, String] the name of the attribute
51
+ # @param type [Class, nil] the type of the attribute (optional)
52
+ # @param options [Hash] additional options for the attribute
53
+ # @option options [Object] :as A default value that makes the attribute read-only.
54
+ # When set, attempts to assign a different value will raise an ArgumentError.
55
+ # This attribute will be included in the compressed hash representation.
56
+ # @option options [Object] :fallback A default value for the attribute.
57
+ # This attribute will be included in the compressed hash representation.
58
+ #
59
+ # @raise [ArgumentError] if attempting to set a value different from the :as default
60
+ #
61
+ # @example Define a read-only attribute with a default value
62
+ # attribute :role, :string, as: "user"
63
+ #
64
+ # @example Define an attribute with a fallback value
65
+ # attribute :temperature, :float, fallback: 0.7
66
+ #
67
+ # @example Define a regular attribute
68
+ # attribute :messages, :array
69
+ def self.attribute(name, type = nil, **options)
70
+ if options.key?(:as)
71
+ default_value = options.delete(:as)
72
+ super(name, type, default: default_value, **options)
73
+
74
+ # Track this attribute as required (must be included in compressed hash)
75
+ required_attributes << name.to_s
76
+
77
+ define_method("#{name}=") do |value|
78
+ normalized_value = value.is_a?(String) ? value.to_sym : value
79
+ normalized_default = default_value.is_a?(String) ? default_value.to_sym : default_value
80
+
81
+ next if normalized_value == normalized_default
82
+
83
+ raise ArgumentError, "Cannot set '#{name}' attribute (read-only with default value)"
84
+ end
85
+ elsif options.key?(:fallback)
86
+ default_value = options.delete(:fallback)
87
+ super(name, type, default: default_value, **options)
88
+
89
+ # Track this attribute as required (must be included in compressed hash)
90
+ required_attributes << name.to_s
91
+ else
92
+ super(name, type, **options)
93
+ end
94
+ end
95
+
96
+ # Delegates attribute accessors to another object.
97
+ #
98
+ # Creates getter and setter methods that forward to the specified target object.
99
+ # If the target is nil when setting, an empty hash is initialized.
100
+ #
101
+ # @param attributes [Array<Symbol>] attribute names to delegate
102
+ # @param to [Symbol] the target method/attribute name
103
+ #
104
+ # @example
105
+ # delegate_attributes :temperature, :max_tokens, to: :options
106
+ def self.delegate_attributes(*attributes, to:)
107
+ attributes.each do |attribute|
108
+ define_method(attribute) do
109
+ public_send(to)&.public_send(attribute)
110
+ end
111
+
112
+ define_method("#{attribute}=") do |value|
113
+ public_send("#{to}=", {}) if public_send(to).nil?
114
+
115
+ public_send(to).public_send("#{attribute}=", value)
116
+ end
117
+ end
118
+ end
119
+
120
+ # Drops specified attributes by defining no-op setters.
121
+ #
122
+ # This is useful when converting between providers that support different attributes
123
+ # or when dropping attributes during message response to request construction.
124
+ # The attributes can still be read if defined elsewhere, but setting them has no effect.
125
+ #
126
+ # @param attributes [Array<Symbol>] attribute names to drop
127
+ # @return [void]
128
+ #
129
+ # @example
130
+ # drop_attributes :metadata, :extra_info
131
+ def self.drop_attributes(*attributes)
132
+ attributes.each do |attribute|
133
+ define_method("#{attribute}=") do |value|
134
+ # No-Op: Drop the value
135
+ end
136
+ end
137
+ end
138
+
139
+ # Returns all attribute keys including aliases.
140
+ #
141
+ # Combines both the main attribute type keys and any attribute aliases,
142
+ # ensuring all possible attribute names are represented as symbols.
143
+ #
144
+ # @return [Array<Symbol>] array of all attribute keys
145
+ def self.keys
146
+ (attribute_types.keys.map(&:to_sym) | attribute_aliases.keys.map(&:to_sym))
147
+ end
148
+
149
+ # Initializes a new instance with the given attributes.
150
+ #
151
+ # Attributes can be provided as a hash. Hash keys are sorted to prioritize nested
152
+ # objects during initialization for backwards compatibility. A special internal key
153
+ # `__default_values` can be passed to get an instance with only default values
154
+ # without any overrides.
155
+ #
156
+ # @param kwargs [Hash] attributes to initialize the instance with
157
+ # @return [BaseModel] the initialized instance
158
+ #
159
+ # @example
160
+ # Message.new(role: "user", content: "Hello")
161
+ def initialize(kwargs = {})
162
+ # To allow us to get a list of attribute defaults without initialized overrides
163
+ return super(nil) if kwargs.key?(:'__default_values')
164
+
165
+ # Backwards Compatibility: This sorts object construction to the top to protect the assignment
166
+ # of backward compatibility assignments.
167
+ kwargs = kwargs.sort_by { |k, v| v.is_a?(Hash) ? 0 : 1 }.to_h if kwargs.is_a?(Hash)
168
+
169
+ super(kwargs)
170
+ end
171
+
172
+ # Merges the given attributes into the current instance.
173
+ #
174
+ # Only attributes with corresponding setter methods are updated.
175
+ # Keys are symbolized before merging.
176
+ #
177
+ # @param kwargs [Hash] attribute keyword arguments to merge
178
+ # @return [BaseModel] self for method chaining
179
+ def merge!(kwargs = {})
180
+ kwargs.deep_symbolize_keys.each do |key, value|
181
+ public_send("#{key}=", value) if respond_to?("#{key}=")
182
+ end
183
+
184
+ self
185
+ end
186
+
187
+ # Recursively removes nil values and empty collections from a hash.
188
+ #
189
+ # Nested hashes and arrays are processed recursively. Empty hashes and
190
+ # arrays after compaction are also removed.
191
+ #
192
+ # @param kwargs [Hash] hash to compact
193
+ # @return [Hash] compacted hash with nil values and empty collections removed
194
+ #
195
+ # @example
196
+ # deep_compact({ a: 1, b: nil, c: { d: nil, e: 2 } })
197
+ # #=> { a: 1, c: { e: 2 } }
198
+ def deep_compact(kwargs = {})
199
+ kwargs.each_with_object({}) do |(key, value), result|
200
+ compacted_value = case value
201
+ when Hash
202
+ deep_compacted = deep_compact(value)
203
+ deep_compacted unless deep_compacted.empty?
204
+ when Array
205
+ compacted_array = value.map { |v| v.is_a?(Hash) ? deep_compact(v) : v }.compact
206
+ compacted_array unless compacted_array.empty?
207
+ else
208
+ value
209
+ end
210
+
211
+ result[key] = compacted_value unless compacted_value.nil?
212
+ end
213
+ end
214
+
215
+ # Converts the model to a hash representation.
216
+ #
217
+ # Recursively converts nested BaseModel instances and arrays to hashes.
218
+ # Nil values and empty collections are removed via deep_compact.
219
+ #
220
+ # @return [Hash] hash representation of all attributes
221
+ #
222
+ # @example
223
+ # message.to_hash
224
+ # #=> { role: "user", content: "Hello", metadata: { id: 1 } }
225
+ def to_hash
226
+ deep_compact(attribute_names.each_with_object({}) do |name, hash|
227
+ value = public_send(name)
228
+
229
+ hash[name.to_sym] = case value
230
+ when BaseModel then value.to_hash
231
+ when Array then value.map { _1.is_a?(BaseModel) ? _1.to_hash : _1 }
232
+ else
233
+ value
234
+ end
235
+ end)
236
+ end
237
+
238
+ # Alias for {#to_hash}.
239
+ #
240
+ # @return [Hash] hash representation of all attributes
241
+ # @see #to_hash
242
+ def to_h = to_hash
243
+
244
+ # Creates a deep duplicate of the model.
245
+ #
246
+ # Duplicates the model instance and recursively duplicates any array or hash attributes
247
+ # to ensure complete independence from the original object.
248
+ #
249
+ # @return [BaseModel] deep duplicate of the model
250
+ def deep_dup
251
+ dup.tap do |duplicated|
252
+ attribute_names.each do |name|
253
+ value = public_send(name)
254
+ next if value.nil?
255
+
256
+ duplicated.public_send("#{name}=", case value
257
+ when Array
258
+ value.map { |v| v.respond_to?(:deep_dup) ? v.deep_dup : v.dup rescue v }
259
+ when Hash
260
+ value.deep_dup
261
+ when BaseModel
262
+ value.deep_dup
263
+ else
264
+ value.dup rescue value
265
+ end)
266
+ end
267
+ end
268
+ end
269
+
270
+ # Serializes the model using attribute type serializers.
271
+ #
272
+ # Iterates through each attribute and uses its ActiveModel::Type serializer
273
+ # to convert the value to its serialized form. Only non-default values are included,
274
+ # except for required attributes (those defined with `:as` or `:fallback` options).
275
+ # This provides a compressed serialization that respects custom type logic.
276
+ #
277
+ # @return [Hash] serialized representation with non-default and required attributes
278
+ #
279
+ # @example
280
+ # message = Message.new(role: "user", content: "Hello")
281
+ # message.serialize #=> { role: "user", content: "Hello" }
282
+ def serialize
283
+ default_values = self.class.new(__default_values: true).attributes
284
+ required_attrs = self.class.required_attributes
285
+
286
+ deep_compact(attribute_names.each_with_object({}) do |name, hash|
287
+ value = public_send(name)
288
+
289
+ # Always include required attributes (those defined with 'as' option)
290
+ # or attributes that differ from their default value
291
+ next if value == default_values[name] && !required_attrs.include?(name)
292
+
293
+ # Use the attribute's type serializer
294
+ attr_type = self.class.attribute_types[name]
295
+ hash[name.to_sym] = attr_type.serialize(value)
296
+ end)
297
+ end
298
+
299
+ # Returns a string representation for inspection.
300
+ #
301
+ # Provides a readable view of the model showing the class name and non-default attributes
302
+ # in a format similar to standard Ruby object inspection.
303
+ #
304
+ # @return [String] formatted string representation
305
+ # @see #serialize
306
+ #
307
+ # @example
308
+ # message = Message.new(role: "user", content: "Hello")
309
+ # message.inspect
310
+ # #=> "#<Message role: \"user\", content: \"Hello\">"
311
+ def inspect
312
+ attrs = JSON.pretty_generate(serialize, {
313
+ space: " ",
314
+ indent: " ",
315
+ object_nl: "\n",
316
+ array_nl: "\n"
317
+ }).lines.drop(1).join.chomp.sub(/\}\z/, "").strip
318
+
319
+ return "#<#{self.class.name}>" if attrs.empty?
320
+
321
+ "#<#{self.class.name} {\n #{attrs}\n}>"
322
+ end
323
+
324
+ # @see #inspect
325
+ alias_method :to_s, :inspect
326
+
327
+ # Compares two models based on their serialized representations.
328
+ #
329
+ # Uses the serialized hash to compare models, allowing for sorting and equality
330
+ # comparisons based on attribute values rather than object identity.
331
+ #
332
+ # @param other [BaseModel] the model to compare against
333
+ # @return [Integer, nil] -1, 0, 1, or nil if not comparable
334
+ #
335
+ # @example
336
+ # model1 = Message.new(content: "A")
337
+ # model2 = Message.new(content: "B")
338
+ # model1 <=> model2 #=> -1
339
+ def <=>(other)
340
+ serialize <=> other&.serialize
341
+ end
342
+
343
+ # Compares equality based on serialized representation.
344
+ #
345
+ # Two models are equal if their serialized hashes are equal, regardless
346
+ # of object identity. This allows value-based equality comparisons.
347
+ #
348
+ # @param other [BaseModel] the model to compare against
349
+ # @return [Boolean]
350
+ #
351
+ # @example
352
+ # model1 = Message.new(content: "Hello")
353
+ # model2 = Message.new(content: "Hello")
354
+ # model1 == model2 #=> true
355
+ def ==(other)
356
+ serialize == other&.serialize
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "responses/prompt"
4
+ require_relative "responses/embed"
5
+
6
+ module ActiveAgent
7
+ module Providers
8
+ module Common
9
+ PromptResponse = Responses::Prompt
10
+ EmbedResponse = Responses::Embed
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../messages/_types"
4
+
5
+ require_relative "format"
6
+
7
+ module ActiveAgent
8
+ module Providers
9
+ module Common
10
+ module Responses
11
+ module Types
12
+ # Type for Messages array - delegates to the shared common messages type
13
+ class MessagesType < Common::Messages::Types::MessagesType
14
+ end
15
+
16
+ class FormatType < ActiveModel::Type::Value
17
+ def cast(value)
18
+ case value
19
+ when BaseModel
20
+ Responses::Format.new(**value.serialize)
21
+ when Hash
22
+ Responses::Format.new(**value.deep_symbolize_keys)
23
+ when nil
24
+ nil
25
+ else
26
+ raise ArgumentError, "Cannot cast #{value.class} to Format"
27
+ end
28
+ end
29
+
30
+ def serialize(value)
31
+ case value
32
+ when Format
33
+ value.serialize
34
+ when Hash
35
+ value
36
+ when nil
37
+ nil
38
+ else
39
+ raise ArgumentError, "Cannot serialize #{value.class}"
40
+ end
41
+ end
42
+
43
+ def deserialize(value)
44
+ cast(value)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_agent/providers/common/model"
4
+ require "active_agent/providers/common/usage"
5
+
6
+ module ActiveAgent
7
+ module Providers
8
+ module Common
9
+ module Responses
10
+ # Provides unified interface for AI provider responses across OpenAI, Anthropic, etc.
11
+ #
12
+ # @abstract Subclass and override {#usage} if provider uses non-standard format
13
+ #
14
+ # @note Use specialized subclasses for specific response types:
15
+ # - {Prompt} for conversational/completion responses
16
+ # - {Embed} for embedding responses
17
+ #
18
+ # @example Accessing response data
19
+ # response = agent.prompt.generate_now
20
+ # response.success? #=> true
21
+ # response.usage #=> Usage object with normalized fields
22
+ # response.total_tokens #=> 30
23
+ #
24
+ # @example Inspecting raw provider data
25
+ # response.raw_request #=> { "model" => "gpt-4", "messages" => [...] }
26
+ # response.raw_response #=> { "id" => "chatcmpl-...", "choices" => [...] }
27
+ #
28
+ # @see Prompt
29
+ # @see Embed
30
+ # @see BaseModel
31
+ class Base < BaseModel
32
+ # @!attribute [r] context
33
+ # Original request context sent to the provider.
34
+ #
35
+ # Includes instructions, messages, tools, and configuration.
36
+ #
37
+ # @return [Hash]
38
+ attribute :context, writable: false
39
+
40
+ # @!attribute [r] raw_request
41
+ # Most recent request in provider-specific format.
42
+ #
43
+ # Useful for debugging and logging.
44
+ #
45
+ # @return [Hash]
46
+ attribute :raw_request, writable: false
47
+
48
+ # @!attribute [r] raw_response
49
+ # Most recent response in provider-specific format.
50
+ #
51
+ # Includes metadata, usage stats, and provider-specific fields.
52
+ # Hash keys are deep symbolized for consistent access.
53
+ #
54
+ # @return [Hash]
55
+ attribute :raw_response, writable: false
56
+
57
+ # @!attribute [r] usages
58
+ # Usage objects from each API call in multi-turn conversations.
59
+ #
60
+ # Each call (e.g., for tool calling) tracks usage separately. These are
61
+ # summed to provide cumulative statistics via {#usage}.
62
+ #
63
+ # @return [Array<Usage>]
64
+ attribute :usages, default: -> { [] }, writable: false
65
+
66
+ # Initializes response with deep-duplicated attributes.
67
+ #
68
+ # Deep duplication prevents external modifications from affecting internal state.
69
+ # The raw_response is deep symbolized for consistent key access across providers.
70
+ #
71
+ # @param kwargs [Hash]
72
+ # @option kwargs [Hash] :context
73
+ # @option kwargs [Hash] :raw_request
74
+ # @option kwargs [Hash] :raw_response
75
+ def initialize(kwargs = {})
76
+ kwargs = kwargs.deep_dup # Ensure that userland can't fuck with our memory space
77
+
78
+ # Deep symbolize raw_response for consistent access across all extraction methods
79
+ if kwargs[:raw_response].is_a?(Hash)
80
+ kwargs[:raw_response] = kwargs[:raw_response].deep_symbolize_keys
81
+ end
82
+
83
+ super(kwargs)
84
+ end
85
+
86
+ # @return [String, Array<Hash>, nil]
87
+ def instructions
88
+ context[:instructions]
89
+ end
90
+
91
+ # @todo Better handling of failure flows
92
+ # @return [Boolean]
93
+ def success?
94
+ true
95
+ end
96
+
97
+ # Normalized usage statistics across all providers.
98
+ #
99
+ # For multi-turn conversations with tool calling, returns cumulative
100
+ # usage across all API calls (sum of {#usages}).
101
+ #
102
+ # @return [Usage, nil]
103
+ #
104
+ # @example Single-turn usage
105
+ # response.usage.input_tokens #=> 100
106
+ # response.usage.output_tokens #=> 25
107
+ # response.usage.total_tokens #=> 125
108
+ #
109
+ # @example Multi-turn usage (cumulative)
110
+ # # After 3 API calls due to tool usage:
111
+ # response.usage.input_tokens #=> 350 (sum of all calls)
112
+ # response.usage.output_tokens #=> 120 (sum of all calls)
113
+ #
114
+ # @see Usage
115
+ def usage
116
+ @usage ||= begin
117
+ if usages.any?
118
+ usages.reduce(:+)
119
+ elsif raw_response
120
+ Usage.from_provider_usage(
121
+ raw_response.is_a?(Hash) ? raw_response[:usage] : raw_response.usage
122
+ )
123
+ end
124
+ end
125
+ end
126
+
127
+ # Response ID from provider, useful for tracking and debugging.
128
+ #
129
+ # @return [String, nil]
130
+ #
131
+ # @example
132
+ # response.id #=> "chatcmpl-CbDx1nXoNSBrNIMhiuy5fk7jXQjmT" (OpenAI)
133
+ # response.id #=> "msg_01RotDmSnYpKQjrTpaHUaEBz" (Anthropic)
134
+ # response.id #=> "gen-1761505659-yxgaVsqVABMQqw6oA7QF" (OpenRouter)
135
+ def id
136
+ @id ||= begin
137
+ return nil unless raw_response
138
+
139
+ if raw_response.is_a?(Hash)
140
+ raw_response[:id]
141
+ elsif raw_response.respond_to?(:id)
142
+ raw_response.id
143
+ end
144
+ end
145
+ end
146
+
147
+ # Model name from provider response.
148
+ #
149
+ # Useful for confirming which model was actually used, as providers may
150
+ # use different versions than requested.
151
+ #
152
+ # @return [String, nil]
153
+ #
154
+ # @example
155
+ # response.model #=> "gpt-4o-mini-2024-07-18"
156
+ # response.model #=> "claude-3-5-haiku-20241022"
157
+ def model
158
+ @model ||= begin
159
+ return nil unless raw_response
160
+
161
+ if raw_response.is_a?(Hash)
162
+ raw_response[:model]
163
+ elsif raw_response.respond_to?(:model)
164
+ raw_response.model
165
+ end
166
+ end
167
+ end
168
+
169
+ # Finish reason from provider response.
170
+ #
171
+ # Indicates why generation stopped (e.g., "stop", "length", "tool_calls").
172
+ # Normalizes access across providers that use different field names.
173
+ #
174
+ # @return [String, nil]
175
+ #
176
+ # @example
177
+ # response.finish_reason #=> "stop"
178
+ # response.finish_reason #=> "length"
179
+ # response.finish_reason #=> "tool_calls"
180
+ # response.stop_reason #=> "stop" (alias)
181
+ def finish_reason
182
+ @finish_reason ||= begin
183
+ return nil unless raw_response
184
+
185
+ if raw_response.is_a?(Hash)
186
+ # OpenAI format: choices[0].finish_reason or choices[0].message.finish_reason
187
+ raw_response.dig(:choices, 0, :finish_reason) ||
188
+ raw_response.dig(:choices, 0, :message, :finish_reason) ||
189
+ # Anthropic format: stop_reason
190
+ raw_response[:stop_reason]
191
+ end
192
+ end
193
+ end
194
+ alias_method :stop_reason, :finish_reason
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Common
8
+ module Responses
9
+ # Response model for embedding responses
10
+ #
11
+ # This class represents responses from embedding endpoints.
12
+ # It includes the embedding data, the original context, raw API data,
13
+ # and usage statistics.
14
+ #
15
+ # == Example
16
+ #
17
+ # response = EmbedResponse.new(
18
+ # context: context_hash,
19
+ # data: [embedding_array],
20
+ # raw_response: { "usage" => { "prompt_tokens" => 10 } }
21
+ # )
22
+ #
23
+ # response.data #=> [[0.1, 0.2, ...]]
24
+ # response.prompt_tokens #=> 10
25
+ # response.usage #=> { "prompt_tokens" => 10, ... }
26
+ class Embed < Base
27
+ # The embedding data
28
+ attribute :data, writable: false
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_agent/providers/common/model"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Common
8
+ module Responses
9
+ class Format < Common::BaseModel
10
+ # Type of response format (text, json_object, json_schema)
11
+ attribute :type, :string, default: "text"
12
+ attribute :name, :string
13
+ attribute :schema
14
+
15
+ validates :type, inclusion: { in: %w[text json_object json_object] }, allow_nil: true
16
+
17
+ # OpenAI's Responses => Common Format
18
+ def format=(value)
19
+ self.type = value[:type]
20
+ end
21
+
22
+ # OpenAI's Chat => Common Format
23
+ def json_schema=(value)
24
+ self.name = value[:name] if value[:name]
25
+ self.schema = value[:schema] if value[:schema]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../messages/base"