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 +4 -4
- data/lib/active_agent/action_prompt/base.rb +3 -0
- data/lib/active_agent/action_prompt/message.rb +17 -2
- data/lib/active_agent/generation_provider/anthropic_provider.rb +1 -0
- data/lib/active_agent/generation_provider/open_ai_provider.rb +2 -1
- data/lib/active_agent/railtie/schema_generator_extension.rb +19 -0
- data/lib/active_agent/railtie.rb +1 -0
- data/lib/active_agent/schema_generator.rb +265 -0
- data/lib/active_agent/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 974e983d62e399bd41c40a34fc0869730d157a8361d74236e327b9aea7cd05e2
|
4
|
+
data.tar.gz: 827c43ed21428ddc4138ecb8465af4265d432fbe3b3ca666f46f1b92b3598a44
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
@
|
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
|
data/lib/active_agent/railtie.rb
CHANGED
@@ -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
|
data/lib/active_agent/version.rb
CHANGED
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.
|
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
|