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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +240 -2
- data/README.md +15 -24
- data/lib/active_agent/base.rb +389 -39
- data/lib/active_agent/concerns/callbacks.rb +251 -0
- data/lib/active_agent/concerns/observers.rb +147 -0
- data/lib/active_agent/concerns/parameterized.rb +292 -0
- data/lib/active_agent/concerns/provider.rb +120 -0
- data/lib/active_agent/concerns/queueing.rb +36 -0
- data/lib/active_agent/concerns/rescue.rb +64 -0
- data/lib/active_agent/concerns/streaming.rb +282 -0
- data/lib/active_agent/concerns/tooling.rb +23 -0
- data/lib/active_agent/concerns/view.rb +150 -0
- data/lib/active_agent/configuration.rb +442 -20
- data/lib/active_agent/generation.rb +141 -47
- data/lib/active_agent/providers/_base_provider.rb +420 -0
- data/lib/active_agent/providers/anthropic/_types.rb +63 -0
- data/lib/active_agent/providers/anthropic/options.rb +53 -0
- data/lib/active_agent/providers/anthropic/request.rb +163 -0
- data/lib/active_agent/providers/anthropic/transforms.rb +353 -0
- data/lib/active_agent/providers/anthropic_provider.rb +254 -0
- data/lib/active_agent/providers/common/messages/_types.rb +160 -0
- data/lib/active_agent/providers/common/messages/assistant.rb +57 -0
- data/lib/active_agent/providers/common/messages/base.rb +17 -0
- data/lib/active_agent/providers/common/messages/system.rb +20 -0
- data/lib/active_agent/providers/common/messages/tool.rb +21 -0
- data/lib/active_agent/providers/common/messages/user.rb +20 -0
- data/lib/active_agent/providers/common/model.rb +361 -0
- data/lib/active_agent/providers/common/response.rb +13 -0
- data/lib/active_agent/providers/common/responses/_types.rb +51 -0
- data/lib/active_agent/providers/common/responses/base.rb +199 -0
- data/lib/active_agent/providers/common/responses/embed.rb +33 -0
- data/lib/active_agent/providers/common/responses/format.rb +31 -0
- data/lib/active_agent/providers/common/responses/message.rb +3 -0
- data/lib/active_agent/providers/common/responses/prompt.rb +42 -0
- data/lib/active_agent/providers/common/usage.rb +385 -0
- data/lib/active_agent/providers/concerns/exception_handler.rb +72 -0
- data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
- data/lib/active_agent/providers/concerns/previewable.rb +150 -0
- data/lib/active_agent/providers/log_subscriber.rb +178 -0
- data/lib/active_agent/providers/mock/_types.rb +77 -0
- data/lib/active_agent/providers/mock/embedding_request.rb +17 -0
- data/lib/active_agent/providers/mock/messages/_types.rb +103 -0
- data/lib/active_agent/providers/mock/messages/assistant.rb +26 -0
- data/lib/active_agent/providers/mock/messages/base.rb +63 -0
- data/lib/active_agent/providers/mock/messages/user.rb +18 -0
- data/lib/active_agent/providers/mock/options.rb +30 -0
- data/lib/active_agent/providers/mock/request.rb +38 -0
- data/lib/active_agent/providers/mock_provider.rb +311 -0
- data/lib/active_agent/providers/ollama/_types.rb +5 -0
- data/lib/active_agent/providers/ollama/chat/_types.rb +44 -0
- data/lib/active_agent/providers/ollama/chat/request.rb +249 -0
- data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
- data/lib/active_agent/providers/ollama/embedding/_types.rb +44 -0
- data/lib/active_agent/providers/ollama/embedding/request.rb +190 -0
- data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
- data/lib/active_agent/providers/ollama/options.rb +27 -0
- data/lib/active_agent/providers/ollama_provider.rb +94 -0
- data/lib/active_agent/providers/open_ai/_base.rb +59 -0
- data/lib/active_agent/providers/open_ai/_types.rb +5 -0
- data/lib/active_agent/providers/open_ai/chat/_types.rb +56 -0
- data/lib/active_agent/providers/open_ai/chat/request.rb +161 -0
- data/lib/active_agent/providers/open_ai/chat/transforms.rb +364 -0
- data/lib/active_agent/providers/open_ai/chat_provider.rb +219 -0
- data/lib/active_agent/providers/open_ai/embedding/_types.rb +56 -0
- data/lib/active_agent/providers/open_ai/embedding/request.rb +53 -0
- data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
- data/lib/active_agent/providers/open_ai/options.rb +74 -0
- data/lib/active_agent/providers/open_ai/responses/_types.rb +44 -0
- data/lib/active_agent/providers/open_ai/responses/request.rb +129 -0
- data/lib/active_agent/providers/open_ai/responses/transforms.rb +228 -0
- data/lib/active_agent/providers/open_ai/responses_provider.rb +200 -0
- data/lib/active_agent/providers/open_ai_provider.rb +94 -0
- data/lib/active_agent/providers/open_router/_types.rb +71 -0
- data/lib/active_agent/providers/open_router/options.rb +141 -0
- data/lib/active_agent/providers/open_router/request.rb +249 -0
- data/lib/active_agent/providers/open_router/requests/_types.rb +197 -0
- data/lib/active_agent/providers/open_router/requests/messages/_types.rb +56 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/_types.rb +97 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +43 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/files/_types.rb +61 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +37 -0
- data/lib/active_agent/providers/open_router/requests/plugin.rb +41 -0
- data/lib/active_agent/providers/open_router/requests/plugins/_types.rb +46 -0
- data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +51 -0
- data/lib/active_agent/providers/open_router/requests/prediction.rb +34 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences/_types.rb +44 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +64 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +105 -0
- data/lib/active_agent/providers/open_router/requests/response_format.rb +77 -0
- data/lib/active_agent/providers/open_router/transforms.rb +134 -0
- data/lib/active_agent/providers/open_router_provider.rb +62 -0
- data/lib/active_agent/providers/openai_provider.rb +2 -0
- data/lib/active_agent/providers/openrouter_provider.rb +2 -0
- data/lib/active_agent/railtie.rb +8 -6
- data/lib/active_agent/schema_generator.rb +333 -166
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +112 -36
- data/lib/generators/active_agent/agent/USAGE +78 -0
- data/lib/generators/active_agent/{agent_generator.rb → agent/agent_generator.rb} +14 -4
- data/lib/generators/active_agent/install/USAGE +25 -0
- data/lib/generators/active_agent/{install_generator.rb → install/install_generator.rb} +1 -19
- data/lib/generators/active_agent/templates/agent.rb.tt +7 -3
- data/lib/generators/active_agent/templates/application_agent.rb.tt +0 -2
- data/lib/generators/erb/agent_generator.rb +31 -16
- data/lib/generators/erb/templates/instructions.md.erb.tt +3 -0
- data/lib/generators/erb/templates/instructions.md.tt +3 -0
- data/lib/generators/erb/templates/instructions.text.tt +1 -0
- data/lib/generators/erb/templates/message.md.erb.tt +5 -0
- data/lib/generators/erb/templates/schema.json.tt +10 -0
- data/lib/generators/test_unit/agent_generator.rb +1 -1
- data/lib/generators/test_unit/templates/functional_test.rb.tt +4 -2
- metadata +182 -71
- data/lib/active_agent/action_prompt/action.rb +0 -13
- data/lib/active_agent/action_prompt/base.rb +0 -623
- data/lib/active_agent/action_prompt/message.rb +0 -126
- data/lib/active_agent/action_prompt/prompt.rb +0 -136
- data/lib/active_agent/action_prompt.rb +0 -19
- data/lib/active_agent/callbacks.rb +0 -33
- data/lib/active_agent/generation_provider/anthropic_provider.rb +0 -163
- data/lib/active_agent/generation_provider/base.rb +0 -55
- data/lib/active_agent/generation_provider/base_adapter.rb +0 -19
- data/lib/active_agent/generation_provider/error_handling.rb +0 -167
- data/lib/active_agent/generation_provider/log_subscriber.rb +0 -92
- data/lib/active_agent/generation_provider/message_formatting.rb +0 -107
- data/lib/active_agent/generation_provider/ollama_provider.rb +0 -66
- data/lib/active_agent/generation_provider/open_ai_provider.rb +0 -279
- data/lib/active_agent/generation_provider/open_router_provider.rb +0 -385
- data/lib/active_agent/generation_provider/parameter_builder.rb +0 -119
- data/lib/active_agent/generation_provider/response.rb +0 -75
- data/lib/active_agent/generation_provider/responses_adapter.rb +0 -44
- data/lib/active_agent/generation_provider/stream_processing.rb +0 -58
- data/lib/active_agent/generation_provider/tool_management.rb +0 -142
- data/lib/active_agent/generation_provider.rb +0 -67
- data/lib/active_agent/log_subscriber.rb +0 -44
- data/lib/active_agent/parameterized.rb +0 -75
- data/lib/active_agent/prompt_helper.rb +0 -19
- data/lib/active_agent/queued_generation.rb +0 -12
- data/lib/active_agent/rescuable.rb +0 -34
- data/lib/active_agent/sanitizers.rb +0 -40
- data/lib/active_agent/streaming.rb +0 -34
- data/lib/active_agent/test_case.rb +0 -125
- data/lib/generators/USAGE +0 -47
- data/lib/generators/active_agent/USAGE +0 -56
- data/lib/generators/erb/install_generator.rb +0 -44
- data/lib/generators/erb/templates/layout.html.erb.tt +0 -1
- data/lib/generators/erb/templates/layout.json.erb.tt +0 -1
- data/lib/generators/erb/templates/layout.text.erb.tt +0 -1
- data/lib/generators/erb/templates/view.html.erb.tt +0 -5
- data/lib/generators/erb/templates/view.json.erb.tt +0 -16
- /data/lib/active_agent/{preview.rb → concerns/preview.rb} +0 -0
- /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
|