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,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Providers
|
|
5
|
+
module OpenAI
|
|
6
|
+
# Handles transformation and normalization of embedding request parameters
|
|
7
|
+
# for the OpenAI Embeddings API
|
|
8
|
+
module Embedding
|
|
9
|
+
# Provides transformation methods for normalizing embedding parameters
|
|
10
|
+
# to OpenAI gem's native format
|
|
11
|
+
module Transforms
|
|
12
|
+
# Converts OpenAI gem objects to hash representation
|
|
13
|
+
#
|
|
14
|
+
# @param obj [Object] gem object or primitive value
|
|
15
|
+
# @return [Hash, Object] hash if object supports JSON serialization, otherwise original object
|
|
16
|
+
def self.gem_to_hash(obj)
|
|
17
|
+
if obj.respond_to?(:to_json)
|
|
18
|
+
JSON.parse(obj.to_json)
|
|
19
|
+
else
|
|
20
|
+
obj
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Normalizes all embedding request parameters
|
|
25
|
+
#
|
|
26
|
+
# @param params [Hash] raw request parameters
|
|
27
|
+
# @return [Hash] normalized parameters
|
|
28
|
+
def self.normalize_params(params)
|
|
29
|
+
normalized = params.dup
|
|
30
|
+
|
|
31
|
+
if normalized[:input]
|
|
32
|
+
normalized[:input] = normalize_input(normalized[:input])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
normalized
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Normalizes input parameter to supported format
|
|
39
|
+
#
|
|
40
|
+
# Handles multiple input formats:
|
|
41
|
+
# - `"text"` - single string for one embedding
|
|
42
|
+
# - `["text1", "text2"]` - array of strings for multiple embeddings
|
|
43
|
+
# - `[1, 2, 3]` - token array for single embedding
|
|
44
|
+
# - `[[1, 2], [3, 4]]` - array of token arrays for multiple embeddings
|
|
45
|
+
#
|
|
46
|
+
# @param input [String, Array<String>, Array<Integer>, Array<Array<Integer>>]
|
|
47
|
+
# @return [String, Array] normalized input in gem-compatible format
|
|
48
|
+
def self.normalize_input(input)
|
|
49
|
+
case input
|
|
50
|
+
when String
|
|
51
|
+
input
|
|
52
|
+
when Array
|
|
53
|
+
if input.empty?
|
|
54
|
+
input
|
|
55
|
+
elsif input.first.is_a?(Integer)
|
|
56
|
+
input
|
|
57
|
+
elsif input.first.is_a?(Array)
|
|
58
|
+
input
|
|
59
|
+
else
|
|
60
|
+
input
|
|
61
|
+
end
|
|
62
|
+
else
|
|
63
|
+
input
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Removes nil values from serialized request
|
|
68
|
+
#
|
|
69
|
+
# @param serialized [Hash] serialized request
|
|
70
|
+
# @param defaults [Hash] default values to remove
|
|
71
|
+
# @param gem_object [Object] original gem object (unused but for consistency)
|
|
72
|
+
# @return [Hash] cleaned request hash
|
|
73
|
+
def self.cleanup_serialized_request(serialized, defaults = {}, gem_object = nil)
|
|
74
|
+
# Remove nil values
|
|
75
|
+
cleaned = serialized.compact
|
|
76
|
+
|
|
77
|
+
# Remove default values
|
|
78
|
+
defaults.each do |key, value|
|
|
79
|
+
cleaned.delete(key) if cleaned[key] == value
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
cleaned
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_agent/providers/common/model"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module OpenAI
|
|
8
|
+
class Options < Common::BaseModel
|
|
9
|
+
attribute :api_key, :string
|
|
10
|
+
attribute :organization, :string # Organization ID
|
|
11
|
+
attribute :project, :string # Project ID ,
|
|
12
|
+
attribute :webhook_secret, :string
|
|
13
|
+
attribute :base_url, :string
|
|
14
|
+
|
|
15
|
+
attribute :max_retries, :integer, default: ::OpenAI::Client::DEFAULT_MAX_RETRIES
|
|
16
|
+
attribute :timeout, :float, default: ::OpenAI::Client::DEFAULT_TIMEOUT_IN_SECONDS
|
|
17
|
+
attribute :initial_retry_delay, :float, default: ::OpenAI::Client::DEFAULT_INITIAL_RETRY_DELAY
|
|
18
|
+
attribute :max_retry_delay, :float, default: ::OpenAI::Client::DEFAULT_MAX_RETRY_DELAY
|
|
19
|
+
|
|
20
|
+
validates :api_key, presence: true
|
|
21
|
+
|
|
22
|
+
# Backwards Compatibility
|
|
23
|
+
alias_attribute :host, :base_url
|
|
24
|
+
alias_attribute :uri_base, :base_url
|
|
25
|
+
alias_attribute :organization_id, :organization
|
|
26
|
+
alias_attribute :project_id, :project
|
|
27
|
+
alias_attribute :access_token, :api_key
|
|
28
|
+
alias_attribute :request_timeout, :timeout
|
|
29
|
+
|
|
30
|
+
# Initialize from a hash (kwargs) with fallback to environment variables and OpenAI gem configuration
|
|
31
|
+
def initialize(kwargs = {})
|
|
32
|
+
kwargs = kwargs.deep_symbolize_keys if kwargs.respond_to?(:deep_symbolize_keys)
|
|
33
|
+
|
|
34
|
+
super(**deep_compact(kwargs.except(:default_url_options).merge(
|
|
35
|
+
api_key: resolve_api_key(kwargs),
|
|
36
|
+
organization_id: resolve_organization_id(kwargs),
|
|
37
|
+
project_id: resolve_project_id(kwargs),
|
|
38
|
+
)))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def extra_headers
|
|
42
|
+
{}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def resolve_api_key(kwargs)
|
|
48
|
+
kwargs[:api_key] ||
|
|
49
|
+
kwargs[:access_token] ||
|
|
50
|
+
ENV["OPENAI_API_KEY"] ||
|
|
51
|
+
ENV["OPEN_AI_API_KEY"] ||
|
|
52
|
+
ENV["OPENAI_ACCESS_TOKEN"] ||
|
|
53
|
+
ENV["OPEN_AI_ACCESS_TOKEN"]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def resolve_organization_id(kwargs)
|
|
57
|
+
kwargs[:organization] ||
|
|
58
|
+
kwargs[:organization_id] ||
|
|
59
|
+
ENV["OPENAI_ORG_ID"] ||
|
|
60
|
+
ENV["OPEN_AI_ORG_ID"] ||
|
|
61
|
+
ENV["OPENAI_ORGANIZATION_ID"] ||
|
|
62
|
+
ENV["OPEN_AI_ORGANIZATION_ID"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def resolve_project_id(kwargs)
|
|
66
|
+
kwargs[:project] ||
|
|
67
|
+
kwargs[:project_id]
|
|
68
|
+
ENV["OPENAI_PROJECT_ID"] ||
|
|
69
|
+
ENV["OPEN_AI_PROJECT_ID"]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "request"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module OpenAI
|
|
8
|
+
module Responses
|
|
9
|
+
# Type for Request model
|
|
10
|
+
class RequestType < ActiveModel::Type::Value
|
|
11
|
+
def cast(value)
|
|
12
|
+
case value
|
|
13
|
+
when Request
|
|
14
|
+
value
|
|
15
|
+
when Hash
|
|
16
|
+
Request.new(**value.deep_symbolize_keys)
|
|
17
|
+
when nil
|
|
18
|
+
nil
|
|
19
|
+
else
|
|
20
|
+
raise ArgumentError, "Cannot cast #{value.class} to Request"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def serialize(value)
|
|
25
|
+
case value
|
|
26
|
+
when Request
|
|
27
|
+
value.serialize
|
|
28
|
+
when Hash
|
|
29
|
+
value
|
|
30
|
+
when nil
|
|
31
|
+
nil
|
|
32
|
+
else
|
|
33
|
+
raise ArgumentError, "Cannot serialize #{value.class}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def deserialize(value)
|
|
38
|
+
cast(value)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "transforms"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module OpenAI
|
|
8
|
+
module Responses
|
|
9
|
+
# Wraps OpenAI gem's ResponseCreateParams with field mapping and normalization
|
|
10
|
+
#
|
|
11
|
+
# Delegates to OpenAI::Models::Responses::ResponseCreateParams while providing
|
|
12
|
+
# compatibility layer for common format fields and content normalization:
|
|
13
|
+
# - `messages` → `input`
|
|
14
|
+
# - `response_format` → `text` (via ResponseTextConfig)
|
|
15
|
+
# - Instructions array joined to string
|
|
16
|
+
class Request < SimpleDelegator
|
|
17
|
+
# Default parameter values applied during initialization
|
|
18
|
+
DEFAULTS = {
|
|
19
|
+
service_tier: "auto",
|
|
20
|
+
store: true,
|
|
21
|
+
temperature: 1.0,
|
|
22
|
+
top_p: 1.0,
|
|
23
|
+
truncation: "disabled",
|
|
24
|
+
parallel_tool_calls: true,
|
|
25
|
+
background: false,
|
|
26
|
+
include: []
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# @return [Boolean, nil]
|
|
30
|
+
attr_reader :stream
|
|
31
|
+
|
|
32
|
+
# @return [Hash, nil]
|
|
33
|
+
attr_reader :response_format
|
|
34
|
+
|
|
35
|
+
# Creates a new response creation request
|
|
36
|
+
#
|
|
37
|
+
# Maps common format fields to Responses API format:
|
|
38
|
+
# - `messages` → `input`
|
|
39
|
+
# - `response_format` → `text` parameter
|
|
40
|
+
# - Instructions array → joined string
|
|
41
|
+
#
|
|
42
|
+
# @param params [Hash] request parameters
|
|
43
|
+
# @option params [String] :model required model identifier
|
|
44
|
+
# @option params [Array, String, Hash] :input messages or content
|
|
45
|
+
# @option params [Array, String, Hash] :messages alternative to :input (mapped automatically)
|
|
46
|
+
# @option params [Hash, String, Symbol] :response_format
|
|
47
|
+
# @option params [Array<String>, String] :instructions
|
|
48
|
+
# @option params [Integer] :max_output_tokens
|
|
49
|
+
# @raise [ArgumentError] when parameters are invalid
|
|
50
|
+
def initialize(**params)
|
|
51
|
+
# Step 1: Extract custom fields
|
|
52
|
+
@stream = params[:stream]
|
|
53
|
+
@response_format = params.delete(:response_format)
|
|
54
|
+
|
|
55
|
+
# Step 2: Map common format 'messages' to OpenAI Responses 'input'
|
|
56
|
+
if params.key?(:messages)
|
|
57
|
+
params[:input] = params.delete(:messages)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Step 3: Join instructions array into string (like Chat API)
|
|
61
|
+
if params[:instructions].is_a?(Array)
|
|
62
|
+
params[:instructions] = params[:instructions].join("\n")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Step 4: Map response_format to text parameter for Responses API
|
|
66
|
+
if @response_format
|
|
67
|
+
params[:text] = Responses::Transforms.normalize_response_format(@response_format)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Step 5: Apply defaults
|
|
71
|
+
params = apply_defaults(params)
|
|
72
|
+
|
|
73
|
+
# Step 6: Normalize input content for gem compatibility
|
|
74
|
+
params[:input] = Responses::Transforms.normalize_input(params[:input]) if params[:input]
|
|
75
|
+
|
|
76
|
+
# Step 7: Create gem model - delegates to OpenAI gem
|
|
77
|
+
gem_model = ::OpenAI::Models::Responses::ResponseCreateParams.new(**params)
|
|
78
|
+
|
|
79
|
+
# Step 8: Delegate all method calls to gem model
|
|
80
|
+
super(gem_model)
|
|
81
|
+
rescue ArgumentError => e
|
|
82
|
+
# Re-raise with more context
|
|
83
|
+
raise ArgumentError, "Invalid OpenAI Responses request parameters: #{e.message}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Serializes request for API call
|
|
87
|
+
#
|
|
88
|
+
# Uses gem's JSON serialization and delegates cleanup to Transforms module.
|
|
89
|
+
#
|
|
90
|
+
# @return [Hash] cleaned request hash
|
|
91
|
+
def serialize
|
|
92
|
+
hash = Responses::Transforms.gem_to_hash(__getobj__)
|
|
93
|
+
Responses::Transforms.cleanup_serialized_request(hash, DEFAULTS, __getobj__)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @return [Array, String, Hash, nil]
|
|
97
|
+
def messages
|
|
98
|
+
__getobj__.instance_variable_get(:@data)[:input]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Sets input messages with normalization
|
|
102
|
+
#
|
|
103
|
+
# @param value [Array, String, Hash]
|
|
104
|
+
# @return [void]
|
|
105
|
+
def messages=(value)
|
|
106
|
+
normalized_value = Responses::Transforms.normalize_input(value)
|
|
107
|
+
__getobj__.instance_variable_get(:@data)[:input] = normalized_value
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
alias_method :message, :messages
|
|
111
|
+
alias_method :message=, :messages=
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# @api private
|
|
116
|
+
# @param params [Hash]
|
|
117
|
+
# @return [Hash]
|
|
118
|
+
def apply_defaults(params)
|
|
119
|
+
DEFAULTS.each do |key, value|
|
|
120
|
+
params[key] = value unless params.key?(key)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
params
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/hash/keys"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module OpenAI
|
|
8
|
+
module Responses
|
|
9
|
+
# Provides transformation methods for normalizing response parameters
|
|
10
|
+
# to OpenAI gem's native format
|
|
11
|
+
#
|
|
12
|
+
# Handles input normalization, message conversion, and response format
|
|
13
|
+
# transformation for the Responses API.
|
|
14
|
+
module Transforms
|
|
15
|
+
class << self
|
|
16
|
+
# Converts gem model object to hash via JSON round-trip
|
|
17
|
+
#
|
|
18
|
+
# @param gem_object [Object]
|
|
19
|
+
# @return [Hash] with symbolized keys
|
|
20
|
+
def gem_to_hash(gem_object)
|
|
21
|
+
JSON.parse(gem_object.to_json, symbolize_names: true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Simplifies input for cleaner API requests
|
|
25
|
+
#
|
|
26
|
+
# Unwraps single-element arrays:
|
|
27
|
+
# - `["text"]` → `"text"`
|
|
28
|
+
# - `[{type: "input_text", text: "..."}]` → `"..."`
|
|
29
|
+
# - `[{role: "user", content: "..."}]` → `"..."`
|
|
30
|
+
#
|
|
31
|
+
# @param input [Array, String, Hash]
|
|
32
|
+
# @return [String, Array, Hash]
|
|
33
|
+
def simplify_input(input)
|
|
34
|
+
return input unless input.is_a?(Array)
|
|
35
|
+
|
|
36
|
+
# Single string element - unwrap it
|
|
37
|
+
if input.size == 1 && input[0].is_a?(String)
|
|
38
|
+
return input[0]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Single content object {type: "input_text", text: "..."} - unwrap to string
|
|
42
|
+
if input.size == 1 &&
|
|
43
|
+
input[0].is_a?(Hash) &&
|
|
44
|
+
input[0][:type] == "input_text" &&
|
|
45
|
+
input[0][:text].is_a?(String) &&
|
|
46
|
+
input[0].keys.sort == [ :text, :type ]
|
|
47
|
+
return input[0][:text]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Single message with string content - simplify to string
|
|
51
|
+
if input.size == 1 &&
|
|
52
|
+
input[0].is_a?(Hash) &&
|
|
53
|
+
input[0][:role] == "user" &&
|
|
54
|
+
input[0][:content].is_a?(String)
|
|
55
|
+
return input[0][:content]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
input
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Normalizes response_format to OpenAI Responses API text parameter
|
|
62
|
+
#
|
|
63
|
+
# Maps common response_format structures to Responses API format.
|
|
64
|
+
# Returns ResponseTextConfig object to preserve proper nesting.
|
|
65
|
+
#
|
|
66
|
+
# @param format [Hash, Symbol, String]
|
|
67
|
+
# @return [OpenAI::Models::Responses::ResponseTextConfig]
|
|
68
|
+
def normalize_response_format(format)
|
|
69
|
+
text_hash = case format
|
|
70
|
+
when Hash
|
|
71
|
+
if format[:type] == "json_schema" || format[:type] == :json_schema
|
|
72
|
+
# json_schema format: map to Responses API structure
|
|
73
|
+
{
|
|
74
|
+
format: {
|
|
75
|
+
type: "json_schema",
|
|
76
|
+
name: format[:name] || format[:json_schema]&.dig(:name),
|
|
77
|
+
schema: format[:schema] || format[:json_schema]&.dig(:schema),
|
|
78
|
+
strict: format[:strict] || format[:json_schema]&.dig(:strict)
|
|
79
|
+
}.compact
|
|
80
|
+
}
|
|
81
|
+
elsif format[:type] == "json_object" || format[:type] == :json_object
|
|
82
|
+
# json_object format
|
|
83
|
+
{ format: { type: "json_object" } }
|
|
84
|
+
elsif format[:type]
|
|
85
|
+
# Other simple type formats (text, etc.) - wrap in format key
|
|
86
|
+
{ format: { type: format[:type].to_s } }
|
|
87
|
+
else
|
|
88
|
+
# Pass through other hash formats (already has format key or complex structure)
|
|
89
|
+
format
|
|
90
|
+
end
|
|
91
|
+
when Symbol, String
|
|
92
|
+
# Simple format types
|
|
93
|
+
{ format: { type: format.to_s } }
|
|
94
|
+
else
|
|
95
|
+
format
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Convert hash to ResponseTextConfig object to preserve nesting
|
|
99
|
+
::OpenAI::Models::Responses::ResponseTextConfig.new(**text_hash)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Normalizes input/messages to gem-compatible format
|
|
103
|
+
#
|
|
104
|
+
# Handles various input formats:
|
|
105
|
+
# - `"text"` → string (passthrough)
|
|
106
|
+
# - `{role: "user", content: "..."}` → wrapped in array
|
|
107
|
+
# - `[{text: "..."}, {image: "url"}]` → wrapped as user message with content array
|
|
108
|
+
# - `["msg1", "msg2"]` → array of user messages
|
|
109
|
+
#
|
|
110
|
+
# @param input [String, Hash, Array, Object]
|
|
111
|
+
# @return [String, Array<Hash>]
|
|
112
|
+
def normalize_input(input)
|
|
113
|
+
# String inputs pass through unchanged
|
|
114
|
+
return input if input.is_a?(String)
|
|
115
|
+
|
|
116
|
+
# Single hash should be wrapped in an array
|
|
117
|
+
if input.is_a?(Hash)
|
|
118
|
+
return [ normalize_message(input) ]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Handle arrays
|
|
122
|
+
return input unless input.respond_to?(:map)
|
|
123
|
+
|
|
124
|
+
# Check if this is an array of content items (strings or text/image/document hashes)
|
|
125
|
+
# Content items don't have a :role key (messages do)
|
|
126
|
+
# BUT NOT a single string (which should have been caught above)
|
|
127
|
+
all_content_items = input.size > 1 && input.all? do |item|
|
|
128
|
+
if item.is_a?(String)
|
|
129
|
+
true
|
|
130
|
+
elsif item.is_a?(Hash)
|
|
131
|
+
# If it has a role, it's a message, not a content item
|
|
132
|
+
!item.key?(:role) && (item.key?(:text) || item.key?(:image) || item.key?(:document))
|
|
133
|
+
else
|
|
134
|
+
false
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
if all_content_items
|
|
139
|
+
# These are multiple content items, wrap in a user message
|
|
140
|
+
content = input.map { |item| normalize_message(item) }
|
|
141
|
+
return [ { role: "user", content: content } ]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Otherwise treat as array of messages
|
|
145
|
+
input.map { |item| normalize_message(item, context: :input) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Normalizes a single message to hash format
|
|
149
|
+
#
|
|
150
|
+
# Handles shorthand formats:
|
|
151
|
+
# - `{text: "..."}` → user message
|
|
152
|
+
# - `{image: "url"}` → input_image content part
|
|
153
|
+
# - `{document: "url"}` → input_file content part
|
|
154
|
+
#
|
|
155
|
+
# @param message [Hash, String, Object]
|
|
156
|
+
# @param context [Symbol] :input for messages, :content for content parts
|
|
157
|
+
# @return [Hash, String]
|
|
158
|
+
def normalize_message(message, context: :content)
|
|
159
|
+
# If it's our custom model object, serialize it
|
|
160
|
+
if message.respond_to?(:serialize)
|
|
161
|
+
message.serialize
|
|
162
|
+
elsif message.is_a?(Hash)
|
|
163
|
+
# If it has a role, it's a message - convert :text to :content
|
|
164
|
+
if message.key?(:role)
|
|
165
|
+
normalized = message.dup
|
|
166
|
+
if normalized.key?(:text) && !normalized.key?(:content)
|
|
167
|
+
normalized[:content] = normalized.delete(:text)
|
|
168
|
+
end
|
|
169
|
+
return normalized
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Expand shorthand formats to full structures for content items
|
|
173
|
+
if message.key?(:image)
|
|
174
|
+
{ type: "input_image", image_url: message[:image] }
|
|
175
|
+
elsif message.key?(:document)
|
|
176
|
+
document_value = message[:document]
|
|
177
|
+
if document_value.start_with?("data:")
|
|
178
|
+
{ type: "input_file", filename: "document.pdf", file_data: document_value }
|
|
179
|
+
else
|
|
180
|
+
{ type: "input_file", file_url: document_value }
|
|
181
|
+
end
|
|
182
|
+
elsif message.key?(:text) && message.size == 1
|
|
183
|
+
# Single :text key without :role - treat as user message
|
|
184
|
+
{ role: "user", content: message[:text] }
|
|
185
|
+
elsif message.key?(:text)
|
|
186
|
+
# Bare text content item with other keys
|
|
187
|
+
{ type: "input_text", text: message[:text] }
|
|
188
|
+
else
|
|
189
|
+
message
|
|
190
|
+
end
|
|
191
|
+
elsif message.is_a?(String)
|
|
192
|
+
# Context matters: in input array, strings become messages; in content array, they become input_text
|
|
193
|
+
if context == :input
|
|
194
|
+
{ role: "user", content: message }
|
|
195
|
+
else
|
|
196
|
+
{ type: "input_text", text: message }
|
|
197
|
+
end
|
|
198
|
+
else
|
|
199
|
+
# Pass through anything else
|
|
200
|
+
message
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Cleans up serialized request for API submission
|
|
205
|
+
#
|
|
206
|
+
# Removes default values and simplifies input where possible.
|
|
207
|
+
#
|
|
208
|
+
# @param hash [Hash] serialized request
|
|
209
|
+
# @param defaults [Hash] default values to remove
|
|
210
|
+
# @param gem_object [Object] original gem object
|
|
211
|
+
# @return [Hash] cleaned request hash
|
|
212
|
+
def cleanup_serialized_request(hash, defaults, gem_object)
|
|
213
|
+
# Remove default values that shouldn't be in the request body
|
|
214
|
+
defaults.each do |key, value|
|
|
215
|
+
hash.delete(key) if hash[key] == value
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Simplify input when possible for cleaner API requests
|
|
219
|
+
hash[:input] = simplify_input(hash[:input]) if hash[:input]
|
|
220
|
+
|
|
221
|
+
hash
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|