activeagent 0.6.0rc4 → 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: 805b4d2cce3173788f3bb281e40973d8782891937b5220654b7dad53a9f701ec
4
- data.tar.gz: 968f534f94d4e32541f1d87e21854add0f4bf8c9e851ba75f7599291a0b384b9
3
+ metadata.gz: 974e983d62e399bd41c40a34fc0869730d157a8361d74236e327b9aea7cd05e2
4
+ data.tar.gz: 827c43ed21428ddc4138ecb8465af4265d432fbe3b3ca666f46f1b92b3598a44
5
5
  SHA512:
6
- metadata.gz: ec13c2dd2d8deb4fd4adb1a6f302de6a7bb5057fad1cd45f27fdecc6b8bea7b5496f3078e7a1f40168de9b4efaa8e4a2abf1cf1bd2865fa929912c58adce561c
7
- data.tar.gz: a564772159ac558c8350d770977fc9b7b509c814435a748cac61af89bd87dc196614b45cc9f33db7bce8951568f659730bfe4cf95fd274f145ae00bb78510af6
6
+ metadata.gz: 7ede550eb0aae62274dd13b631f862f202b172bb6ba40f837358bac71cc58760edc444722bc5a0aea747760b6317cf33630754992866fdc439a8263c1b670c03
7
+ data.tar.gz: 80671e11f47c81ef591ed496c9a1ee126f32762d588787d3cd15a79eaf229f7da0b1c8e69cedfbd33509df92030a399ae2faaa5c346f9cf1cbedfdc25ee80819
data/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
  <source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/2bad263a-c09f-40b6-94ba-fff8e346d65d">
3
3
  <img alt="activeagents_banner" src="https://github.com/user-attachments/assets/0ebbaa2f-c6bf-4d40-bb77-931015a14be3">
4
4
  </picture>
5
- *Build AI in Rails*
6
5
 
7
6
 
7
+ > *Build AI in Rails*
8
8
  >
9
9
  > *Now Agents are Controllers*
10
10
  >
@@ -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: {
@@ -280,7 +283,7 @@ module ActiveAgent
280
283
  def initialize # :nodoc:
281
284
  super
282
285
  @_prompt_was_called = false
283
- @_context = ActiveAgent::ActionPrompt::Prompt.new(options: self.class.options || {}, agent_instance: self)
286
+ @_context = ActiveAgent::ActionPrompt::Prompt.new(options: self.class.options&.deep_dup || {}, agent_instance: self)
284
287
  end
285
288
 
286
289
  def process(method_name, *args) # :nodoc:
@@ -444,7 +447,10 @@ module ActiveAgent
444
447
  :presence_penalty, :response_format, :seed, :stop, :tools_choice, :plugins,
445
448
 
446
449
  # OpenRouter Provider Settings
447
- :data_collection, :require_parameters, :only, :ignore, :quantizations, :sort, :max_price
450
+ :data_collection, :require_parameters, :only, :ignore, :quantizations, :sort, :max_price,
451
+
452
+ # Built-in Tools Support (OpenAI Responses API)
453
+ :tools
448
454
  )
449
455
  # Handle explicit options parameter
450
456
  explicit_options = prompt_options[:options] || {}
@@ -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]
@@ -7,6 +7,7 @@ module ActiveAgent
7
7
  included do
8
8
  include ActiveSupport::Callbacks
9
9
  define_callbacks :generation, skip_after_callbacks_if_terminated: true
10
+ define_callbacks :embedding, skip_after_callbacks_if_terminated: true
10
11
  end
11
12
 
12
13
  module ClassMethods
@@ -18,6 +19,14 @@ module ActiveAgent
18
19
  set_callback(:generation, callback, name, options)
19
20
  end
20
21
  end
22
+
23
+ # # Defines a callback that will get called right before/after/around the
24
+ # # embedding provider method.
25
+ define_method "#{callback}_embedding" do |*names, &blk|
26
+ _insert_callbacks(names, blk) do |name, options|
27
+ set_callback(:embedding, callback, name, options)
28
+ end
29
+ end
21
30
  end
22
31
  end
23
32
  end
@@ -50,6 +50,18 @@ module ActiveAgent
50
50
  end
51
51
  end
52
52
 
53
+ def embed_now
54
+ processed_agent.handle_exceptions do
55
+ processed_agent.run_callbacks(:embedding) do
56
+ processed_agent.embed
57
+ end
58
+ end
59
+ end
60
+
61
+ def embed_later(options = {})
62
+ enqueue_generation :embed_now, options
63
+ end
64
+
53
65
  private
54
66
 
55
67
  def processed_agent
@@ -4,7 +4,7 @@ begin
4
4
  gem "ruby-anthropic", "~> 0.4.2"
5
5
  require "anthropic"
6
6
  rescue LoadError
7
- raise LoadError, "The 'ruby-anthropic' gem is required for AnthropicProvider. Please add it to your Gemfile and run `bundle install`."
7
+ raise LoadError, "The 'ruby-anthropic ~> 0.4.2' gem is required for AnthropicProvider. Please add it to your Gemfile and run `bundle install`."
8
8
  end
9
9
 
10
10
  require "active_agent/action_prompt/action"
@@ -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),
@@ -60,7 +60,7 @@ module ActiveAgent
60
60
  end
61
61
 
62
62
  def format_error_message(error)
63
- message = if error.respond_to?(:response)
63
+ message = if error.respond_to?(:response) && error.response
64
64
  error.response[:body]
65
65
  elsif error.respond_to?(:message)
66
66
  error.message
@@ -9,7 +9,56 @@ module ActiveAgent
9
9
  @access_token ||= config["api_key"] || config["access_token"] || ENV["OLLAMA_API_KEY"] || ENV["OLLAMA_ACCESS_TOKEN"]
10
10
  @model_name = config["model"]
11
11
  @host = config["host"] || "http://localhost:11434"
12
- @client = OpenAI::Client.new(uri_base: @host, access_token: @access_token, log_errors: true)
12
+ @api_version = config["api_version"] || "v1"
13
+ @client = OpenAI::Client.new(uri_base: @host, access_token: @access_token, log_errors: Rails.env.development?, api_version: @api_version)
14
+ end
15
+
16
+ protected
17
+
18
+ def format_error_message(error)
19
+ # Check for various connection-related errors
20
+ connection_errors = [
21
+ Errno::ECONNREFUSED,
22
+ Errno::EADDRNOTAVAIL,
23
+ Errno::EHOSTUNREACH,
24
+ Net::OpenTimeout,
25
+ Net::ReadTimeout,
26
+ SocketError,
27
+ Faraday::ConnectionFailed
28
+ ]
29
+
30
+ if connection_errors.any? { |klass| error.is_a?(klass) } ||
31
+ (error.message&.include?("Failed to open TCP connection") ||
32
+ error.message&.include?("Connection refused"))
33
+ "Unable to connect to Ollama at #{@host}. Please ensure Ollama is running on the configured host and port.\n" \
34
+ "You can start Ollama with: `ollama serve`\n" \
35
+ "Or update your configuration to point to the correct host."
36
+ else
37
+ super
38
+ end
39
+ end
40
+
41
+ def embeddings_parameters(input: prompt.message.content, model: "nomic-embed-text")
42
+ {
43
+ model: @config["embedding_model"] || model,
44
+ input: input
45
+ }
46
+ end
47
+
48
+ def embeddings_response(response, request_params = nil)
49
+ # Ollama can return either format:
50
+ # 1. OpenAI-compatible: { "data": [{ "embedding": [...] }] }
51
+ # 2. Native Ollama: { "embedding": [...] }
52
+ embedding = response.dig("data", 0, "embedding") || response.dig("embedding")
53
+
54
+ message = ActiveAgent::ActionPrompt::Message.new(content: embedding, role: "assistant")
55
+
56
+ @response = ActiveAgent::GenerationProvider::Response.new(
57
+ prompt: prompt,
58
+ message: message,
59
+ raw_response: response,
60
+ raw_request: request_params
61
+ )
13
62
  end
14
63
  end
15
64
  end
@@ -2,7 +2,7 @@ begin
2
2
  gem "ruby-openai", ">= 8.1.0"
3
3
  require "openai"
4
4
  rescue LoadError
5
- raise LoadError, "The 'ruby-openai' gem is required for OpenAIProvider. Please add it to your Gemfile and run `bundle install`."
5
+ raise LoadError, "The 'ruby-openai >= 8.1.0' gem is required for OpenAIProvider. Please add it to your Gemfile and run `bundle install`."
6
6
  end
7
7
 
8
8
  require "active_agent/action_prompt/action"
@@ -31,7 +31,7 @@ module ActiveAgent
31
31
  organization_id: @organization_id,
32
32
  admin_token: @admin_token,
33
33
  log_errors: Rails.env.development?
34
- )
34
+ )
35
35
 
36
36
  @model_name = config["model"] || "gpt-4o-mini"
37
37
  end
@@ -92,10 +92,39 @@ module ActiveAgent
92
92
 
93
93
  private
94
94
 
95
- # Now using modules, but we can override build_provider_parameters for OpenAI-specific needs
96
- # The prompt_parameters method comes from ParameterBuilder module
97
- # The format_tools method comes from ToolManagement module
98
- # The provider_messages method comes from MessageFormatting module
95
+ # Override from ParameterBuilder to add web_search_options for Chat API
96
+ def build_provider_parameters
97
+ params = {}
98
+
99
+ # Check if we're using a model that supports web_search_options in Chat API
100
+ if chat_api_web_search_model? && @prompt.options[:web_search]
101
+ params[:web_search_options] = build_web_search_options(@prompt.options[:web_search])
102
+ end
103
+
104
+ params
105
+ end
106
+
107
+ def chat_api_web_search_model?
108
+ model = @prompt.options[:model] || @model_name
109
+ [ "gpt-4o-search-preview", "gpt-4o-mini-search-preview" ].include?(model)
110
+ end
111
+
112
+ def build_web_search_options(web_search_config)
113
+ options = {}
114
+
115
+ if web_search_config.is_a?(Hash)
116
+ options[:search_context_size] = web_search_config[:search_context_size] if web_search_config[:search_context_size]
117
+
118
+ if web_search_config[:user_location]
119
+ options[:user_location] = {
120
+ type: "approximate",
121
+ approximate: web_search_config[:user_location]
122
+ }
123
+ end
124
+ end
125
+
126
+ options
127
+ end
99
128
 
100
129
  def chat_response(response, request_params = nil)
101
130
  return @response if prompt.options[:stream]
@@ -123,7 +152,7 @@ module ActiveAgent
123
152
  role: message_json["role"].intern,
124
153
  action_requested: message_json["finish_reason"] == "tool_calls",
125
154
  raw_actions: message_json["tool_calls"] || [],
126
- content_type: prompt.output_schema.present? ? "application/json" : "text/plain",
155
+ content_type: prompt.output_schema.present? ? "application/json" : "text/plain"
127
156
  )
128
157
 
129
158
  @response = ActiveAgent::GenerationProvider::Response.new(
@@ -141,7 +170,8 @@ module ActiveAgent
141
170
  role: message_json["role"].intern,
142
171
  action_requested: message_json["finish_reason"] == "tool_calls",
143
172
  raw_actions: message_json["tool_calls"] || [],
144
- 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"
145
175
  )
146
176
  end
147
177
 
@@ -161,14 +191,65 @@ module ActiveAgent
161
191
  end
162
192
 
163
193
  def responses_parameters(model: @prompt.options[:model] || @model_name, messages: @prompt.messages, temperature: @prompt.options[:temperature] || @config["temperature"] || 0.7, tools: @prompt.actions, structured_output: @prompt.output_schema)
194
+ # Build tools array, combining action tools with built-in tools
195
+ tools_array = build_tools_for_responses(tools)
196
+
164
197
  {
165
198
  model: model,
166
199
  input: ActiveAgent::GenerationProvider::ResponsesAdapter.new(@prompt).input,
167
- tools: tools.presence,
200
+ tools: tools_array.presence,
168
201
  text: structured_output
169
202
  }.compact
170
203
  end
171
204
 
205
+ def build_tools_for_responses(action_tools)
206
+ tools = []
207
+
208
+ # Start with action tools (user-defined functions) if any
209
+ tools.concat(action_tools) if action_tools.present?
210
+
211
+ # Add built-in tools if specified in options[:tools]
212
+ if @prompt.options[:tools].present?
213
+ built_in_tools = @prompt.options[:tools]
214
+ built_in_tools = [ built_in_tools ] unless built_in_tools.is_a?(Array)
215
+
216
+ built_in_tools.each do |tool|
217
+ next unless tool.is_a?(Hash)
218
+
219
+ case tool[:type]
220
+ when "web_search_preview", "web_search"
221
+ web_search_tool = { type: "web_search_preview" }
222
+ web_search_tool[:search_context_size] = tool[:search_context_size] if tool[:search_context_size]
223
+ web_search_tool[:user_location] = tool[:user_location] if tool[:user_location]
224
+ tools << web_search_tool
225
+
226
+ when "image_generation"
227
+ image_gen_tool = { type: "image_generation" }
228
+ image_gen_tool[:size] = tool[:size] if tool[:size]
229
+ image_gen_tool[:quality] = tool[:quality] if tool[:quality]
230
+ image_gen_tool[:format] = tool[:format] if tool[:format]
231
+ image_gen_tool[:compression] = tool[:compression] if tool[:compression]
232
+ image_gen_tool[:background] = tool[:background] if tool[:background]
233
+ image_gen_tool[:partial_images] = tool[:partial_images] if tool[:partial_images]
234
+ tools << image_gen_tool
235
+
236
+ when "mcp"
237
+ mcp_tool = { type: "mcp" }
238
+ mcp_tool[:server_label] = tool[:server_label] if tool[:server_label]
239
+ mcp_tool[:server_description] = tool[:server_description] if tool[:server_description]
240
+ mcp_tool[:server_url] = tool[:server_url] if tool[:server_url]
241
+ mcp_tool[:connector_id] = tool[:connector_id] if tool[:connector_id]
242
+ mcp_tool[:authorization] = tool[:authorization] if tool[:authorization]
243
+ mcp_tool[:require_approval] = tool[:require_approval] if tool[:require_approval]
244
+ mcp_tool[:allowed_tools] = tool[:allowed_tools] if tool[:allowed_tools]
245
+ tools << mcp_tool
246
+ end
247
+ end
248
+ end
249
+
250
+ tools
251
+ end
252
+
172
253
  def embeddings_parameters(input: prompt.message.content, model: "text-embedding-3-large")
173
254
  {
174
255
  model: model,
@@ -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.0rc4"
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.0rc4
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
@@ -249,6 +249,34 @@ dependencies:
249
249
  - - ">="
250
250
  - !ruby/object:Gem::Version
251
251
  version: '0'
252
+ - !ruby/object:Gem::Dependency
253
+ name: cuprite
254
+ requirement: !ruby/object:Gem::Requirement
255
+ requirements:
256
+ - - "~>"
257
+ - !ruby/object:Gem::Version
258
+ version: '0.15'
259
+ type: :development
260
+ prerelease: false
261
+ version_requirements: !ruby/object:Gem::Requirement
262
+ requirements:
263
+ - - "~>"
264
+ - !ruby/object:Gem::Version
265
+ version: '0.15'
266
+ - !ruby/object:Gem::Dependency
267
+ name: capybara
268
+ requirement: !ruby/object:Gem::Requirement
269
+ requirements:
270
+ - - "~>"
271
+ - !ruby/object:Gem::Version
272
+ version: '3.40'
273
+ type: :development
274
+ prerelease: false
275
+ version_requirements: !ruby/object:Gem::Requirement
276
+ requirements:
277
+ - - "~>"
278
+ - !ruby/object:Gem::Version
279
+ version: '3.40'
252
280
  description: The only agent-oriented AI framework designed for Rails, where Agents
253
281
  are Controllers. Build AI features with less complexity using the MVC conventions
254
282
  you love.
@@ -295,8 +323,10 @@ files:
295
323
  - lib/active_agent/prompt_helper.rb
296
324
  - lib/active_agent/queued_generation.rb
297
325
  - lib/active_agent/railtie.rb
326
+ - lib/active_agent/railtie/schema_generator_extension.rb
298
327
  - lib/active_agent/rescuable.rb
299
328
  - lib/active_agent/sanitizers.rb
329
+ - lib/active_agent/schema_generator.rb
300
330
  - lib/active_agent/service.rb
301
331
  - lib/active_agent/streaming.rb
302
332
  - lib/active_agent/test_case.rb