activeagent 0.6.0 → 0.6.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56c61e9a6a6173d572792952121413f72d49d9b9bcee932a350666d845236465
4
- data.tar.gz: 86267a64dea6c8eef7bf890fe10fcb1ef1a2b2d09ddeab3f0a9d1d25bb6465fd
3
+ metadata.gz: 974e983d62e399bd41c40a34fc0869730d157a8361d74236e327b9aea7cd05e2
4
+ data.tar.gz: 827c43ed21428ddc4138ecb8465af4265d432fbe3b3ca666f46f1b92b3598a44
5
5
  SHA512:
6
- metadata.gz: c173eac89c75335d474c533a41f3137ef2279c5aef1a90d60c93270383dd77c1e15b557c1e006e41c6f15f47f0fce12502c924fd939d58cac85978b97d749d2f
7
- data.tar.gz: 6bd6d686a0a1a2613206f8b6d50d093a343e7913bd0dbdaac669abb95895a0306f114cc77e2cea01280981b5b5fff403d0edc6de1b1912cfc822ecc2935de961
6
+ metadata.gz: 7ede550eb0aae62274dd13b631f862f202b172bb6ba40f837358bac71cc58760edc444722bc5a0aea747760b6317cf33630754992866fdc439a8263c1b670c03
7
+ data.tar.gz: 80671e11f47c81ef591ed496c9a1ee126f32762d588787d3cd15a79eaf229f7da0b1c8e69cedfbd33509df92030a399ae2faaa5c346f9cf1cbedfdc25ee80819
@@ -36,6 +36,9 @@ module ActiveAgent
36
36
 
37
37
  helper ActiveAgent::PromptHelper
38
38
 
39
+ # Delegate response to generation_provider for easy access in callbacks
40
+ delegate :response, to: :generation_provider, allow_nil: true
41
+
39
42
  class_attribute :options
40
43
 
41
44
  class_attribute :default_params, default: {
@@ -18,7 +18,7 @@ module ActiveAgent
18
18
  end
19
19
  VALID_ROLES = %w[system assistant user tool].freeze
20
20
 
21
- attr_accessor :action_id, :action_name, :raw_actions, :generation_id, :content, :role, :action_requested, :requested_actions, :content_type, :charset, :metadata
21
+ attr_accessor :action_id, :action_name, :raw_actions, :generation_id, :content, :raw_content, :role, :action_requested, :requested_actions, :content_type, :charset, :metadata
22
22
 
23
23
  def initialize(attributes = {})
24
24
  @action_id = attributes[:action_id]
@@ -26,8 +26,9 @@ module ActiveAgent
26
26
  @generation_id = attributes[:generation_id]
27
27
  @metadata = attributes[:metadata] || {}
28
28
  @charset = attributes[:charset] || "UTF-8"
29
- @content = attributes[:content] || ""
29
+ @raw_content = attributes[:content] || ""
30
30
  @content_type = detect_content_type(attributes)
31
+ @content = parse_content(@raw_content, @content_type)
31
32
  @role = attributes[:role] || :user
32
33
  @raw_actions = attributes[:raw_actions]
33
34
  @requested_actions = attributes[:requested_actions] || []
@@ -85,6 +86,20 @@ module ActiveAgent
85
86
 
86
87
  private
87
88
 
89
+ def parse_content(content, content_type)
90
+ # Auto-parse JSON content if content_type indicates JSON
91
+ if content_type&.match?(/json/i) && content.is_a?(String) && !content.empty?
92
+ begin
93
+ JSON.parse(content)
94
+ rescue JSON::ParserError
95
+ # If parsing fails, return the raw content
96
+ content
97
+ end
98
+ else
99
+ content
100
+ end
101
+ end
102
+
88
103
  def detect_content_type(attributes)
89
104
  # If content_type is explicitly provided, use it
90
105
  return attributes[:content_type] if attributes[:content_type]
@@ -130,6 +130,7 @@ module ActiveAgent
130
130
 
131
131
  message = ActiveAgent::ActionPrompt::Message.new(
132
132
  content: content,
133
+ content_type: prompt.output_schema.present? ? "application/json" : "text/plain",
133
134
  role: "assistant",
134
135
  action_requested: response["stop_reason"] == "tool_use",
135
136
  requested_actions: handle_actions(response["content"].map { |c| c if c["type"] == "tool_use" }.reject { |m| m.blank? }.to_a),
@@ -170,7 +170,8 @@ module ActiveAgent
170
170
  role: message_json["role"].intern,
171
171
  action_requested: message_json["finish_reason"] == "tool_calls",
172
172
  raw_actions: message_json["tool_calls"] || [],
173
- requested_actions: handle_actions(message_json["tool_calls"])
173
+ requested_actions: handle_actions(message_json["tool_calls"]),
174
+ content_type: prompt.output_schema.present? ? "application/json" : "text/plain"
174
175
  )
175
176
  end
176
177
 
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_agent/schema_generator"
4
+
5
+ module ActiveAgent
6
+ class SchemaGeneratorRailtie < Rails::Railtie
7
+ initializer "active_agent.schema_generator" do
8
+ ActiveSupport.on_load(:active_record) do
9
+ ActiveRecord::Base.include ActiveAgent::SchemaGenerator
10
+ end
11
+
12
+ ActiveSupport.on_load(:active_model) do
13
+ if defined?(ActiveModel::Model)
14
+ ActiveModel::Model.include ActiveAgent::SchemaGenerator
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -5,6 +5,7 @@ require "active_agent"
5
5
  # require "active_agent/engine"
6
6
  require "rails"
7
7
  require "abstract_controller/railties/routes_helpers"
8
+ require "active_agent/railtie/schema_generator_extension"
8
9
 
9
10
  module ActiveAgent
10
11
  class Railtie < Rails::Railtie # :nodoc:
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module SchemaGenerator
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ if defined?(ActiveRecord::Base) && self < ActiveRecord::Base
9
+ extend ActiveRecordClassMethods
10
+ elsif defined?(ActiveModel::Model) && self.included_modules.include?(ActiveModel::Model)
11
+ extend ActiveModelClassMethods
12
+ else
13
+ # Fallback for any class that includes this module
14
+ extend ActiveModelClassMethods
15
+ end
16
+ end
17
+
18
+ module ActiveRecordClassMethods
19
+ def to_json_schema(options = {})
20
+ ActiveAgent::SchemaGenerator::Builder.json_schema_from_model(self, options)
21
+ end
22
+ end
23
+
24
+ module ActiveModelClassMethods
25
+ def to_json_schema(options = {})
26
+ ActiveAgent::SchemaGenerator::Builder.json_schema_from_model(self, options)
27
+ end
28
+ end
29
+
30
+ class Builder
31
+ def self.json_schema_from_model(model_class, options = {})
32
+ schema = {
33
+ type: "object",
34
+ properties: {},
35
+ required: [],
36
+ additionalProperties: options.fetch(:additional_properties, false)
37
+ }
38
+
39
+ if defined?(ActiveRecord::Base) && model_class < ActiveRecord::Base
40
+ build_activerecord_schema(model_class, schema, options)
41
+ elsif defined?(ActiveModel::Model) && model_class.include?(ActiveModel::Model)
42
+ build_activemodel_schema(model_class, schema, options)
43
+ else
44
+ raise ArgumentError, "Model must be an ActiveRecord or ActiveModel class"
45
+ end
46
+
47
+ if options[:strict]
48
+ # OpenAI strict mode requires all properties to be in the required array
49
+ # So we add all properties to required if strict mode is enabled
50
+ schema[:required] = schema[:properties].keys.map(&:to_s).sort
51
+
52
+ {
53
+ name: options[:name] || model_class.name.underscore,
54
+ schema: schema,
55
+ strict: true
56
+ }
57
+ else
58
+ schema
59
+ end
60
+ end
61
+
62
+ class << self
63
+ private
64
+
65
+ def build_activerecord_schema(model_class, schema, options)
66
+ model_class.columns.each do |column|
67
+ next if options[:exclude]&.include?(column.name.to_sym)
68
+ next if column.name == "id" && !options[:include_id]
69
+
70
+ property = build_property_from_column(column)
71
+ schema[:properties][column.name] = property
72
+
73
+ if !column.null && column.name != "id"
74
+ schema[:required] << column.name
75
+ end
76
+ end
77
+
78
+ if model_class.reflect_on_all_associations.any?
79
+ add_associations_to_schema(model_class, schema, options)
80
+ end
81
+
82
+ if model_class.respond_to?(:validators)
83
+ add_validations_to_schema(model_class, schema, options)
84
+ end
85
+ end
86
+
87
+ def build_activemodel_schema(model_class, schema, options)
88
+ if model_class.respond_to?(:attribute_types)
89
+ model_class.attribute_types.each do |name, type|
90
+ next if options[:exclude]&.include?(name.to_sym)
91
+
92
+ property = build_property_from_type(type)
93
+ schema[:properties][name] = property
94
+ end
95
+ end
96
+
97
+ if model_class.respond_to?(:validators)
98
+ add_validations_to_schema(model_class, schema, options)
99
+ end
100
+ end
101
+
102
+ def build_property_from_column(column)
103
+ property = {
104
+ type: map_sql_type_to_json_type(column.type),
105
+ description: "#{column.name.humanize} field"
106
+ }
107
+
108
+ case column.type
109
+ when :string, :text
110
+ if column.limit
111
+ property[:maxLength] = column.limit
112
+ end
113
+ when :integer, :bigint
114
+ property[:type] = "integer"
115
+ when :decimal, :float
116
+ property[:type] = "number"
117
+ when :boolean
118
+ property[:type] = "boolean"
119
+ when :date, :datetime, :timestamp
120
+ property[:type] = "string"
121
+ property[:format] = (column.type == :date) ? "date" : "date-time"
122
+ when :json, :jsonb
123
+ property[:type] = "object"
124
+ else
125
+ property[:type] = "string"
126
+ end
127
+
128
+ if column.default
129
+ property[:default] = column.default
130
+ end
131
+
132
+ property
133
+ end
134
+
135
+ def build_property_from_type(type)
136
+ property = { type: "string" }
137
+
138
+ case type
139
+ when ActiveModel::Type::String
140
+ property[:type] = "string"
141
+ when ActiveModel::Type::Integer
142
+ property[:type] = "integer"
143
+ when ActiveModel::Type::Float, ActiveModel::Type::Decimal
144
+ property[:type] = "number"
145
+ when ActiveModel::Type::Boolean
146
+ property[:type] = "boolean"
147
+ when ActiveModel::Type::Date
148
+ property[:type] = "string"
149
+ property[:format] = "date"
150
+ when ActiveModel::Type::DateTime, ActiveModel::Type::Time
151
+ property[:type] = "string"
152
+ property[:format] = "date-time"
153
+ else
154
+ property[:type] = "string"
155
+ end
156
+
157
+ property
158
+ end
159
+
160
+ def map_sql_type_to_json_type(sql_type)
161
+ case sql_type
162
+ when :string, :text
163
+ "string"
164
+ when :integer, :bigint
165
+ "integer"
166
+ when :decimal, :float
167
+ "number"
168
+ when :boolean
169
+ "boolean"
170
+ when :json, :jsonb
171
+ "object"
172
+ when :array
173
+ "array"
174
+ else
175
+ "string"
176
+ end
177
+ end
178
+
179
+ def add_associations_to_schema(model_class, schema, options)
180
+ return unless options[:include_associations]
181
+
182
+ schema[:$defs] ||= {}
183
+
184
+ model_class.reflect_on_all_associations.each do |association|
185
+ next if options[:exclude_associations]&.include?(association.name)
186
+
187
+ case association.macro
188
+ when :has_many, :has_and_belongs_to_many
189
+ schema[:properties][association.name.to_s] = {
190
+ type: "array",
191
+ items: { "$ref": "#/$defs/#{association.name.to_s.singularize}" }
192
+ }
193
+ if options[:nested_associations]
194
+ nested_schema = json_schema_from_model(
195
+ association.klass,
196
+ options.merge(include_associations: false)
197
+ )
198
+ schema[:$defs][association.name.to_s.singularize] = nested_schema
199
+ end
200
+ when :has_one, :belongs_to
201
+ schema[:properties][association.name.to_s] = {
202
+ "$ref": "#/$defs/#{association.name}"
203
+ }
204
+ if options[:nested_associations]
205
+ nested_schema = json_schema_from_model(
206
+ association.klass,
207
+ options.merge(include_associations: false)
208
+ )
209
+ schema[:$defs][association.name.to_s] = nested_schema
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ def add_validations_to_schema(model_class, schema, options)
216
+ model_class.validators.each do |validator|
217
+ validator.attributes.each do |attribute|
218
+ next unless schema[:properties][attribute.to_s]
219
+
220
+ case validator
221
+ when ActiveModel::Validations::PresenceValidator
222
+ schema[:required] << attribute.to_s unless schema[:required].include?(attribute.to_s)
223
+ when ActiveModel::Validations::LengthValidator
224
+ if validator.options[:minimum]
225
+ schema[:properties][attribute.to_s][:minLength] = validator.options[:minimum]
226
+ end
227
+ if validator.options[:maximum]
228
+ schema[:properties][attribute.to_s][:maxLength] = validator.options[:maximum]
229
+ end
230
+ when ActiveModel::Validations::NumericalityValidator
231
+ if validator.options[:greater_than]
232
+ schema[:properties][attribute.to_s][:exclusiveMinimum] = validator.options[:greater_than]
233
+ end
234
+ if validator.options[:less_than]
235
+ schema[:properties][attribute.to_s][:exclusiveMaximum] = validator.options[:less_than]
236
+ end
237
+ if validator.options[:greater_than_or_equal_to]
238
+ schema[:properties][attribute.to_s][:minimum] = validator.options[:greater_than_or_equal_to]
239
+ end
240
+ if validator.options[:less_than_or_equal_to]
241
+ schema[:properties][attribute.to_s][:maximum] = validator.options[:less_than_or_equal_to]
242
+ end
243
+ when ActiveModel::Validations::InclusionValidator
244
+ if validator.options[:in]
245
+ schema[:properties][attribute.to_s][:enum] = validator.options[:in]
246
+ end
247
+ when ActiveModel::Validations::FormatValidator
248
+ if validator.options[:with] == URI::MailTo::EMAIL_REGEXP
249
+ schema[:properties][attribute.to_s][:format] = "email"
250
+ elsif validator.options[:with]
251
+ schema[:properties][attribute.to_s][:pattern] = validator.options[:with].source
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+
260
+ def generate_schema_view(model_class, options = {})
261
+ schema = ActiveAgent::SchemaGenerator::Builder.json_schema_from_model(model_class, options)
262
+ schema.to_json
263
+ end
264
+ end
265
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiveAgent
2
- VERSION = "0.6.0"
2
+ VERSION = "0.6.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeagent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
@@ -323,8 +323,10 @@ files:
323
323
  - lib/active_agent/prompt_helper.rb
324
324
  - lib/active_agent/queued_generation.rb
325
325
  - lib/active_agent/railtie.rb
326
+ - lib/active_agent/railtie/schema_generator_extension.rb
326
327
  - lib/active_agent/rescuable.rb
327
328
  - lib/active_agent/sanitizers.rb
329
+ - lib/active_agent/schema_generator.rb
328
330
  - lib/active_agent/service.rb
329
331
  - lib/active_agent/streaming.rb
330
332
  - lib/active_agent/test_case.rb