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 +4 -4
- data/README.md +1 -1
- data/lib/active_agent/action_prompt/base.rb +8 -2
- data/lib/active_agent/action_prompt/message.rb +17 -2
- data/lib/active_agent/callbacks.rb +9 -0
- data/lib/active_agent/generation.rb +12 -0
- data/lib/active_agent/generation_provider/anthropic_provider.rb +2 -1
- data/lib/active_agent/generation_provider/error_handling.rb +1 -1
- data/lib/active_agent/generation_provider/ollama_provider.rb +50 -1
- data/lib/active_agent/generation_provider/open_ai_provider.rb +90 -9
- 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 +31 -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
|
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
|
-
@
|
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),
|
@@ -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
|
-
@
|
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
|
-
#
|
96
|
-
|
97
|
-
|
98
|
-
|
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:
|
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
|
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
|
@@ -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
|