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.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +240 -2
  3. data/README.md +15 -24
  4. data/lib/active_agent/base.rb +389 -39
  5. data/lib/active_agent/concerns/callbacks.rb +251 -0
  6. data/lib/active_agent/concerns/observers.rb +147 -0
  7. data/lib/active_agent/concerns/parameterized.rb +292 -0
  8. data/lib/active_agent/concerns/provider.rb +120 -0
  9. data/lib/active_agent/concerns/queueing.rb +36 -0
  10. data/lib/active_agent/concerns/rescue.rb +64 -0
  11. data/lib/active_agent/concerns/streaming.rb +282 -0
  12. data/lib/active_agent/concerns/tooling.rb +23 -0
  13. data/lib/active_agent/concerns/view.rb +150 -0
  14. data/lib/active_agent/configuration.rb +442 -20
  15. data/lib/active_agent/generation.rb +141 -47
  16. data/lib/active_agent/providers/_base_provider.rb +420 -0
  17. data/lib/active_agent/providers/anthropic/_types.rb +63 -0
  18. data/lib/active_agent/providers/anthropic/options.rb +53 -0
  19. data/lib/active_agent/providers/anthropic/request.rb +163 -0
  20. data/lib/active_agent/providers/anthropic/transforms.rb +353 -0
  21. data/lib/active_agent/providers/anthropic_provider.rb +254 -0
  22. data/lib/active_agent/providers/common/messages/_types.rb +160 -0
  23. data/lib/active_agent/providers/common/messages/assistant.rb +57 -0
  24. data/lib/active_agent/providers/common/messages/base.rb +17 -0
  25. data/lib/active_agent/providers/common/messages/system.rb +20 -0
  26. data/lib/active_agent/providers/common/messages/tool.rb +21 -0
  27. data/lib/active_agent/providers/common/messages/user.rb +20 -0
  28. data/lib/active_agent/providers/common/model.rb +361 -0
  29. data/lib/active_agent/providers/common/response.rb +13 -0
  30. data/lib/active_agent/providers/common/responses/_types.rb +51 -0
  31. data/lib/active_agent/providers/common/responses/base.rb +199 -0
  32. data/lib/active_agent/providers/common/responses/embed.rb +33 -0
  33. data/lib/active_agent/providers/common/responses/format.rb +31 -0
  34. data/lib/active_agent/providers/common/responses/message.rb +3 -0
  35. data/lib/active_agent/providers/common/responses/prompt.rb +42 -0
  36. data/lib/active_agent/providers/common/usage.rb +385 -0
  37. data/lib/active_agent/providers/concerns/exception_handler.rb +72 -0
  38. data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
  39. data/lib/active_agent/providers/concerns/previewable.rb +150 -0
  40. data/lib/active_agent/providers/log_subscriber.rb +178 -0
  41. data/lib/active_agent/providers/mock/_types.rb +77 -0
  42. data/lib/active_agent/providers/mock/embedding_request.rb +17 -0
  43. data/lib/active_agent/providers/mock/messages/_types.rb +103 -0
  44. data/lib/active_agent/providers/mock/messages/assistant.rb +26 -0
  45. data/lib/active_agent/providers/mock/messages/base.rb +63 -0
  46. data/lib/active_agent/providers/mock/messages/user.rb +18 -0
  47. data/lib/active_agent/providers/mock/options.rb +30 -0
  48. data/lib/active_agent/providers/mock/request.rb +38 -0
  49. data/lib/active_agent/providers/mock_provider.rb +311 -0
  50. data/lib/active_agent/providers/ollama/_types.rb +5 -0
  51. data/lib/active_agent/providers/ollama/chat/_types.rb +44 -0
  52. data/lib/active_agent/providers/ollama/chat/request.rb +249 -0
  53. data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
  54. data/lib/active_agent/providers/ollama/embedding/_types.rb +44 -0
  55. data/lib/active_agent/providers/ollama/embedding/request.rb +190 -0
  56. data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
  57. data/lib/active_agent/providers/ollama/options.rb +27 -0
  58. data/lib/active_agent/providers/ollama_provider.rb +94 -0
  59. data/lib/active_agent/providers/open_ai/_base.rb +59 -0
  60. data/lib/active_agent/providers/open_ai/_types.rb +5 -0
  61. data/lib/active_agent/providers/open_ai/chat/_types.rb +56 -0
  62. data/lib/active_agent/providers/open_ai/chat/request.rb +161 -0
  63. data/lib/active_agent/providers/open_ai/chat/transforms.rb +364 -0
  64. data/lib/active_agent/providers/open_ai/chat_provider.rb +219 -0
  65. data/lib/active_agent/providers/open_ai/embedding/_types.rb +56 -0
  66. data/lib/active_agent/providers/open_ai/embedding/request.rb +53 -0
  67. data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
  68. data/lib/active_agent/providers/open_ai/options.rb +74 -0
  69. data/lib/active_agent/providers/open_ai/responses/_types.rb +44 -0
  70. data/lib/active_agent/providers/open_ai/responses/request.rb +129 -0
  71. data/lib/active_agent/providers/open_ai/responses/transforms.rb +228 -0
  72. data/lib/active_agent/providers/open_ai/responses_provider.rb +200 -0
  73. data/lib/active_agent/providers/open_ai_provider.rb +94 -0
  74. data/lib/active_agent/providers/open_router/_types.rb +71 -0
  75. data/lib/active_agent/providers/open_router/options.rb +141 -0
  76. data/lib/active_agent/providers/open_router/request.rb +249 -0
  77. data/lib/active_agent/providers/open_router/requests/_types.rb +197 -0
  78. data/lib/active_agent/providers/open_router/requests/messages/_types.rb +56 -0
  79. data/lib/active_agent/providers/open_router/requests/messages/content/_types.rb +97 -0
  80. data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +43 -0
  81. data/lib/active_agent/providers/open_router/requests/messages/content/files/_types.rb +61 -0
  82. data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +37 -0
  83. data/lib/active_agent/providers/open_router/requests/plugin.rb +41 -0
  84. data/lib/active_agent/providers/open_router/requests/plugins/_types.rb +46 -0
  85. data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +51 -0
  86. data/lib/active_agent/providers/open_router/requests/prediction.rb +34 -0
  87. data/lib/active_agent/providers/open_router/requests/provider_preferences/_types.rb +44 -0
  88. data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +64 -0
  89. data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +105 -0
  90. data/lib/active_agent/providers/open_router/requests/response_format.rb +77 -0
  91. data/lib/active_agent/providers/open_router/transforms.rb +134 -0
  92. data/lib/active_agent/providers/open_router_provider.rb +62 -0
  93. data/lib/active_agent/providers/openai_provider.rb +2 -0
  94. data/lib/active_agent/providers/openrouter_provider.rb +2 -0
  95. data/lib/active_agent/railtie.rb +8 -6
  96. data/lib/active_agent/schema_generator.rb +333 -166
  97. data/lib/active_agent/version.rb +1 -1
  98. data/lib/active_agent.rb +112 -36
  99. data/lib/generators/active_agent/agent/USAGE +78 -0
  100. data/lib/generators/active_agent/{agent_generator.rb → agent/agent_generator.rb} +14 -4
  101. data/lib/generators/active_agent/install/USAGE +25 -0
  102. data/lib/generators/active_agent/{install_generator.rb → install/install_generator.rb} +1 -19
  103. data/lib/generators/active_agent/templates/agent.rb.tt +7 -3
  104. data/lib/generators/active_agent/templates/application_agent.rb.tt +0 -2
  105. data/lib/generators/erb/agent_generator.rb +31 -16
  106. data/lib/generators/erb/templates/instructions.md.erb.tt +3 -0
  107. data/lib/generators/erb/templates/instructions.md.tt +3 -0
  108. data/lib/generators/erb/templates/instructions.text.tt +1 -0
  109. data/lib/generators/erb/templates/message.md.erb.tt +5 -0
  110. data/lib/generators/erb/templates/schema.json.tt +10 -0
  111. data/lib/generators/test_unit/agent_generator.rb +1 -1
  112. data/lib/generators/test_unit/templates/functional_test.rb.tt +4 -2
  113. metadata +182 -71
  114. data/lib/active_agent/action_prompt/action.rb +0 -13
  115. data/lib/active_agent/action_prompt/base.rb +0 -623
  116. data/lib/active_agent/action_prompt/message.rb +0 -126
  117. data/lib/active_agent/action_prompt/prompt.rb +0 -136
  118. data/lib/active_agent/action_prompt.rb +0 -19
  119. data/lib/active_agent/callbacks.rb +0 -33
  120. data/lib/active_agent/generation_provider/anthropic_provider.rb +0 -163
  121. data/lib/active_agent/generation_provider/base.rb +0 -55
  122. data/lib/active_agent/generation_provider/base_adapter.rb +0 -19
  123. data/lib/active_agent/generation_provider/error_handling.rb +0 -167
  124. data/lib/active_agent/generation_provider/log_subscriber.rb +0 -92
  125. data/lib/active_agent/generation_provider/message_formatting.rb +0 -107
  126. data/lib/active_agent/generation_provider/ollama_provider.rb +0 -66
  127. data/lib/active_agent/generation_provider/open_ai_provider.rb +0 -279
  128. data/lib/active_agent/generation_provider/open_router_provider.rb +0 -385
  129. data/lib/active_agent/generation_provider/parameter_builder.rb +0 -119
  130. data/lib/active_agent/generation_provider/response.rb +0 -75
  131. data/lib/active_agent/generation_provider/responses_adapter.rb +0 -44
  132. data/lib/active_agent/generation_provider/stream_processing.rb +0 -58
  133. data/lib/active_agent/generation_provider/tool_management.rb +0 -142
  134. data/lib/active_agent/generation_provider.rb +0 -67
  135. data/lib/active_agent/log_subscriber.rb +0 -44
  136. data/lib/active_agent/parameterized.rb +0 -75
  137. data/lib/active_agent/prompt_helper.rb +0 -19
  138. data/lib/active_agent/queued_generation.rb +0 -12
  139. data/lib/active_agent/rescuable.rb +0 -34
  140. data/lib/active_agent/sanitizers.rb +0 -40
  141. data/lib/active_agent/streaming.rb +0 -34
  142. data/lib/active_agent/test_case.rb +0 -125
  143. data/lib/generators/USAGE +0 -47
  144. data/lib/generators/active_agent/USAGE +0 -56
  145. data/lib/generators/erb/install_generator.rb +0 -44
  146. data/lib/generators/erb/templates/layout.html.erb.tt +0 -1
  147. data/lib/generators/erb/templates/layout.json.erb.tt +0 -1
  148. data/lib/generators/erb/templates/layout.text.erb.tt +0 -1
  149. data/lib/generators/erb/templates/view.html.erb.tt +0 -5
  150. data/lib/generators/erb/templates/view.json.erb.tt +0 -16
  151. /data/lib/active_agent/{preview.rb → concerns/preview.rb} +0 -0
  152. /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