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,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Ollama
8
+ # Handles transformation and normalization of embedding request parameters
9
+ # for the Ollama Embeddings API
10
+ module Embedding
11
+ # Provides transformation methods for normalizing embedding parameters
12
+ # to Ollama API format with OpenAI gem compatibility
13
+ module Transforms
14
+ class << self
15
+ # Converts gem objects to hash representation
16
+ #
17
+ # @param obj [Object] gem object or primitive value
18
+ # @return [Hash, Object] hash if object supports JSON serialization
19
+ def gem_to_hash(obj)
20
+ if obj.respond_to?(:to_json)
21
+ JSON.parse(obj.to_json, symbolize_names: true)
22
+ else
23
+ obj
24
+ end
25
+ end
26
+
27
+ # Normalizes all embedding request parameters
28
+ #
29
+ # Ollama-specific parameters (options, keep_alive, truncate) are extracted
30
+ # and returned separately from OpenAI-compatible parameters.
31
+ #
32
+ # @param params [Hash] raw request parameters
33
+ # @return [Array<Hash, Hash>] tuple of [openai_params, ollama_params]
34
+ def normalize_params(params)
35
+ params = params.dup
36
+
37
+ # Extract Ollama-specific parameters
38
+ ollama_params = {}
39
+ ollama_params[:options] = params.delete(:options) if params.key?(:options)
40
+ ollama_params[:keep_alive] = params.delete(:keep_alive) if params.key?(:keep_alive)
41
+ ollama_params[:truncate] = params.delete(:truncate) if params.key?(:truncate)
42
+
43
+ # Extract options attributes that can be at top level
44
+ extract_option_attributes(params, ollama_params)
45
+
46
+ # Normalize input - Ollama only accepts strings, not token arrays
47
+ if params[:input]
48
+ params[:input] = normalize_input(params[:input])
49
+ end
50
+
51
+ [ params, ollama_params ]
52
+ end
53
+
54
+ # Normalizes input parameter to Ollama format
55
+ #
56
+ # Ollama only accepts strings or arrays of strings (no token arrays).
57
+ # Converts single string to array internally for consistency.
58
+ #
59
+ # @param input [String, Array<String>]
60
+ # @return [Array<String>] normalized input as array of strings
61
+ # @raise [ArgumentError] if input contains non-string values
62
+ def normalize_input(input)
63
+ case input
64
+ when String
65
+ [ input.presence ].compact
66
+ when Array
67
+ # Validate all elements are strings
68
+ input.each_with_index do |item, index|
69
+ unless item.is_a?(String)
70
+ raise ArgumentError, "Ollama embedding input must contain only strings, got #{item.class} at index #{index}"
71
+ end
72
+ if item.empty?
73
+ raise ArgumentError, "Ollama embedding input cannot contain empty strings at index #{index}"
74
+ end
75
+ end
76
+ input.compact
77
+ when nil
78
+ nil
79
+ else
80
+ raise ArgumentError, "Cannot normalize #{input.class} to Ollama input (expected String or Array)"
81
+ end
82
+ end
83
+
84
+ # Serializes input for API submission
85
+ #
86
+ # Returns single string if array has only one element, otherwise array.
87
+ #
88
+ # @param input [Array<String>, nil]
89
+ # @return [String, Array<String>, nil]
90
+ def serialize_input(input)
91
+ return nil if input.nil?
92
+
93
+ # Return single string if array has only one element
94
+ if input.is_a?(Array) && input.length == 1
95
+ input.first
96
+ else
97
+ input
98
+ end
99
+ end
100
+
101
+ # Cleans up serialized request for API submission
102
+ #
103
+ # Merges OpenAI-compatible params with Ollama-specific params.
104
+ #
105
+ # @param openai_hash [Hash] serialized OpenAI-compatible request
106
+ # @param ollama_params [Hash] Ollama-specific parameters
107
+ # @param defaults [Hash] default values to remove
108
+ # @return [Hash] cleaned and merged request hash
109
+ def cleanup_serialized_request(openai_hash, ollama_params, defaults)
110
+ # Remove nil values
111
+ cleaned = openai_hash.compact
112
+
113
+ # Serialize input (convert single-element array to string)
114
+ if cleaned[:input]
115
+ cleaned[:input] = serialize_input(cleaned[:input])
116
+ end
117
+
118
+ # Merge Ollama-specific params, skip defaults
119
+ ollama_params.each do |key, value|
120
+ next if value.nil?
121
+ next if value.respond_to?(:empty?) && value.empty?
122
+ next if defaults.key?(key) && defaults[key] == value
123
+
124
+ # Serialize options object if present
125
+ cleaned[key] = if value.respond_to?(:serialize)
126
+ value.serialize
127
+ else
128
+ value
129
+ end
130
+ end
131
+
132
+ cleaned
133
+ end
134
+
135
+ private
136
+
137
+ # Extracts option attributes that can be specified at the top level
138
+ #
139
+ # @param params [Hash] request parameters
140
+ # @param ollama_params [Hash] ollama-specific parameters to populate
141
+ def extract_option_attributes(params, ollama_params)
142
+ option_keys = [
143
+ :mirostat, :mirostat_eta, :mirostat_tau, :num_ctx,
144
+ :repeat_last_n, :repeat_penalty, :temperature, :seed,
145
+ :num_predict, :top_k, :top_p, :min_p, :stop
146
+ ]
147
+
148
+ option_keys.each do |key|
149
+ if params.key?(key)
150
+ ollama_params[:options] ||= {}
151
+ ollama_params[:options][key] = params.delete(key)
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../open_ai/options"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Ollama
8
+ class Options < ActiveAgent::Providers::OpenAI::Options
9
+ attribute :base_url, :string, fallback: "http://127.0.0.1:11434/v1"
10
+ attribute :api_key, :string, fallback: "ollama"
11
+
12
+ private
13
+
14
+ def resolve_api_key(kwargs)
15
+ kwargs[:api_key] ||
16
+ kwargs[:access_token] ||
17
+ ENV["OLLAMA_API_KEY"] ||
18
+ ENV["OLLAMA_ACCESS_TOKEN"]
19
+ end
20
+
21
+ # Not Used as Part of Ollama
22
+ def resolve_organization_id(settings) = nil
23
+ def resolve_project_id(settings) = nil
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,94 @@
1
+ require_relative "_base_provider"
2
+
3
+ require_gem!(:openai, __FILE__)
4
+
5
+ require_relative "open_ai_provider"
6
+ require_relative "ollama/_types"
7
+
8
+ module ActiveAgent
9
+ module Providers
10
+ # Connects to local Ollama instances via OpenAI-compatible API.
11
+ #
12
+ # Provides chat completion and embedding functionality through locally-hosted
13
+ # Ollama models. Handles connection errors specific to local deployments.
14
+ #
15
+ # @see OpenAI::ChatProvider
16
+ class OllamaProvider < OpenAI::ChatProvider
17
+ # @return [String]
18
+ def self.service_name
19
+ "Ollama"
20
+ end
21
+
22
+ # @return [Class]
23
+ def self.options_klass
24
+ namespace::Options
25
+ end
26
+
27
+ # @return [ActiveModel::Type::Value]
28
+ def self.prompt_request_type
29
+ namespace::Chat::RequestType.new
30
+ end
31
+
32
+ # @return [ActiveModel::Type::Value]
33
+ def self.embed_request_type
34
+ namespace::Embedding::RequestType.new
35
+ end
36
+
37
+ protected
38
+
39
+ # Executes chat completion request with Ollama-specific error handling.
40
+ #
41
+ # @see OpenAI::ChatProvider#api_prompt_execute
42
+ # @param parameters [Hash]
43
+ # @return [Object, nil] response object or nil for streaming
44
+ # @raise [OpenAI::Errors::APIConnectionError] when Ollama server unreachable
45
+ def api_prompt_execute(parameters)
46
+ super
47
+
48
+ rescue ::OpenAI::Errors::APIConnectionError => exception
49
+ log_connection_error(exception)
50
+ raise exception
51
+ end
52
+
53
+ # Executes embedding request with Ollama-specific error handling.
54
+ #
55
+ # @param parameters [Hash]
56
+ # @return [Hash] symbolized API response
57
+ # @raise [OpenAI::Errors::APIConnectionError] when Ollama server unreachable
58
+ def api_embed_execute(parameters)
59
+ client.embeddings.create(**parameters).as_json.deep_symbolize_keys
60
+
61
+ rescue ::OpenAI::Errors::APIConnectionError => exception
62
+ log_connection_error(exception)
63
+ raise exception
64
+ end
65
+
66
+ # Handles role duplication bug in Ollama's OpenAI-compatible streaming.
67
+ #
68
+ # Ollama duplicates role information in streaming deltas, requiring
69
+ # manual cleanup to prevent message corruption. This fixes the
70
+ # "role appears in every chunk" issue when using streaming responses.
71
+ #
72
+ # @see OpenAI::ChatProvider#message_merge_delta
73
+ # @param message [Hash]
74
+ # @param delta [Hash]
75
+ # @return [Hash]
76
+ def message_merge_delta(message, delta)
77
+ message[:role] = delta.delete(:role) if delta[:role] # Copy a Bad Design (OpenAI's Chat API) Badly, Win Bad Prizes
78
+
79
+ hash_merge_delta(message, delta)
80
+ end
81
+
82
+ # Logs connection failures with Ollama server details for debugging.
83
+ #
84
+ # @param error [Exception]
85
+ # @return [void]
86
+ def log_connection_error(error)
87
+ instrument("connection_error.provider.active_agent",
88
+ uri_base: options.uri_base,
89
+ exception: error.class,
90
+ message: error.message)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,59 @@
1
+ require_relative "../_base_provider"
2
+
3
+ require_gem!(:openai, __FILE__)
4
+
5
+ require_relative "options"
6
+
7
+ module ActiveAgent
8
+ module Providers
9
+ module OpenAI
10
+ # Base provider class for OpenAI API implementations.
11
+ #
12
+ # Provides common functionality for OpenAI-based providers including
13
+ # client configuration, service identification, and tool call processing.
14
+ #
15
+ # @see ActiveAgent::Providers::BaseProvider
16
+ class Base < ActiveAgent::Providers::BaseProvider
17
+ # Returns the service name for OpenAI providers.
18
+ #
19
+ # @return [String] Always returns "OpenAI"
20
+ def self.service_name
21
+ "OpenAI"
22
+ end
23
+
24
+ # Returns a configured OpenAI client instance.
25
+ #
26
+ # @return [OpenAI::Client] The configured OpenAI client
27
+ def client
28
+ ::OpenAI::Client.new(**options.serialize)
29
+ end
30
+
31
+ protected
32
+
33
+ # Processes a tool call function from the API response.
34
+ #
35
+ # This method extracts the function name and arguments from an API function call,
36
+ # parses the arguments as JSON, and invokes the function callback with the parsed parameters.
37
+ #
38
+ # @param api_function_call [Hash] The function call data from the API response
39
+ # @option api_function_call [String] :name The name of the function to call
40
+ # @option api_function_call [String] :arguments JSON string containing the function arguments
41
+ #
42
+ # @return [Object] The result of the function callback invocation
43
+ #
44
+ # @example Processing a tool call
45
+ # api_call = { name: "get_weather", arguments: '{"location":"NYC"}' }
46
+ # process_tool_call_function(api_call)
47
+ # # => calls tools_function.call("get_weather", location: "NYC")
48
+ def process_tool_call_function(api_function_call)
49
+ name = api_function_call[:name]
50
+ kwargs = JSON.parse(api_function_call[:arguments], symbolize_names: true) if api_function_call[:arguments]
51
+
52
+ instrument("tool_call.active_agent", tool_name: name) do
53
+ tools_function.call(name, **kwargs)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "chat/_types"
4
+ require_relative "embedding/_types"
5
+ require_relative "responses/_types"
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "request"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module OpenAI
8
+ module Chat
9
+ # ActiveModel type for casting and serializing chat requests
10
+ class RequestType < ActiveModel::Type::Value
11
+ # Casts value to Request object
12
+ #
13
+ # @param value [Request, Hash, nil]
14
+ # @return [Request, nil]
15
+ # @raise [ArgumentError] when value cannot be cast
16
+ def cast(value)
17
+ case value
18
+ when Request
19
+ value
20
+ when Hash
21
+ Request.new(**value.deep_symbolize_keys)
22
+ when nil
23
+ nil
24
+ else
25
+ raise ArgumentError, "Cannot cast #{value.class} to Request"
26
+ end
27
+ end
28
+
29
+ # Serializes Request to hash for API submission
30
+ #
31
+ # @param value [Request, Hash, nil]
32
+ # @return [Hash, nil]
33
+ # @raise [ArgumentError] when value cannot be serialized
34
+ def serialize(value)
35
+ case value
36
+ when Request
37
+ value.serialize
38
+ when Hash
39
+ value
40
+ when nil
41
+ nil
42
+ else
43
+ raise ArgumentError, "Cannot serialize #{value.class}"
44
+ end
45
+ end
46
+
47
+ # @param value [Object]
48
+ # @return [Request, nil]
49
+ def deserialize(value)
50
+ cast(value)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+ require "json"
5
+ require_relative "transforms"
6
+
7
+ module ActiveAgent
8
+ module Providers
9
+ module OpenAI
10
+ module Chat
11
+ # Wraps OpenAI gem's CompletionCreateParams with normalization
12
+ #
13
+ # Delegates to OpenAI::Models::Chat::CompletionCreateParams while providing
14
+ # parameter normalization and shorthand format support via the Transforms module.
15
+ #
16
+ # All OpenAI Chat API fields are available via delegation:
17
+ # model, messages, temperature, max_tokens, max_completion_tokens, top_p,
18
+ # frequency_penalty, presence_penalty, tools, tool_choice, response_format,
19
+ # stream_options, audio, prediction, metadata, modalities, service_tier, store,
20
+ # parallel_tool_calls, reasoning_effort, verbosity, stop, seed, logit_bias,
21
+ # logprobs, top_logprobs, prompt_cache_key, safety_identifier, user,
22
+ # web_search_options, function_call, functions
23
+ #
24
+ # @example Basic usage
25
+ # request = Request.new(
26
+ # model: "gpt-4o",
27
+ # messages: [{role: "user", content: "Hello"}]
28
+ # )
29
+ #
30
+ # @example String message normalization
31
+ # Request.new(model: "gpt-4o", messages: "Hello")
32
+ # # Normalized to: [{role: "user", content: "Hello"}]
33
+ #
34
+ # @example Instructions support
35
+ # Request.new(
36
+ # model: "gpt-4o",
37
+ # messages: [{role: "user", content: "Hi"}],
38
+ # instructions: ["You are helpful", "Be concise"]
39
+ # )
40
+ # # instructions converted to developer messages and prepended
41
+ class Request < SimpleDelegator
42
+ # Default parameter values applied during initialization
43
+ DEFAULTS = {
44
+ frequency_penalty: 0,
45
+ logprobs: false,
46
+ modalities: [ "text" ],
47
+ n: 1,
48
+ parallel_tool_calls: true,
49
+ presence_penalty: 0,
50
+ service_tier: "auto",
51
+ store: false,
52
+ stream: false,
53
+ temperature: 1,
54
+ top_p: 1
55
+ }.freeze
56
+
57
+ # @return [Boolean, nil]
58
+ attr_reader :stream
59
+
60
+ # Creates a new chat completion request
61
+ #
62
+ # @param params [Hash] request parameters
63
+ # @option params [String] :model required model identifier
64
+ # @option params [Array, String, Hash] :messages required conversation messages
65
+ # @option params [Array<String>, String] :instructions system/developer prompts
66
+ # @option params [Hash, String, Symbol] :response_format
67
+ # @option params [Float] :temperature (1) sampling temperature 0-2
68
+ # @option params [Integer] :max_tokens maximum tokens to generate
69
+ # @option params [Array] :tools available tool definitions
70
+ # @raise [ArgumentError] when parameters are invalid
71
+ def initialize(**params)
72
+ # Step 1: Extract stream flag
73
+ @stream = params[:stream]
74
+
75
+ # Step 2: Apply defaults
76
+ params = apply_defaults(params)
77
+
78
+ # Step 3: Normalize all parameters (instructions, messages, response_format)
79
+ params = Chat::Transforms.normalize_params(params)
80
+
81
+ # Step 4: Create gem model - this validates all parameters!
82
+ gem_model = ::OpenAI::Models::Chat::CompletionCreateParams.new(**params)
83
+
84
+ # Step 5: Delegate all method calls to gem model
85
+ super(gem_model)
86
+ rescue ArgumentError => e
87
+ # Re-raise with more context
88
+ raise ArgumentError, "Invalid OpenAI Chat request parameters: #{e.message}"
89
+ end
90
+
91
+ # Serializes request for API call
92
+ #
93
+ # Uses gem's JSON serialization, removes default values for minimal
94
+ # request body, and simplifies messages where possible.
95
+ #
96
+ # @return [Hash] cleaned request hash
97
+ def serialize
98
+ # Use gem's JSON serialization (handles all nested objects)
99
+ hash = Chat::Transforms.gem_to_hash(__getobj__)
100
+
101
+ # Cleanup and simplify for API request
102
+ Chat::Transforms.cleanup_serialized_request(hash, DEFAULTS, __getobj__)
103
+ end
104
+
105
+ # @return [Array<Hash>, nil]
106
+ def messages
107
+ __getobj__.instance_variable_get(:@data)[:messages]
108
+ end
109
+
110
+ # Sets messages with normalization
111
+ #
112
+ # @param value [Array, String, Hash]
113
+ # @return [void]
114
+ def messages=(value)
115
+ normalized_value = Chat::Transforms.normalize_messages(value)
116
+ __getobj__.instance_variable_get(:@data)[:messages] = normalized_value
117
+ end
118
+
119
+ # Alias for messages (common format compatibility)
120
+ #
121
+ # @return [Array<Hash>, nil]
122
+ def message
123
+ messages
124
+ end
125
+
126
+ # @param value [Array, String, Hash]
127
+ # @return [void]
128
+ def message=(value)
129
+ self.messages = value
130
+ end
131
+
132
+ # Sets instructions as developer messages
133
+ #
134
+ # Prepends developer messages to the messages array for common format compatibility.
135
+ #
136
+ # @param values [Array<String>, String]
137
+ # @return [void]
138
+ def instructions=(*values)
139
+ instructions_messages = Chat::Transforms.normalize_instructions(values.flatten)
140
+ current_messages = messages || []
141
+ self.messages = instructions_messages + current_messages
142
+ end
143
+
144
+ private
145
+
146
+ # @api private
147
+ # @param params [Hash]
148
+ # @return [Hash]
149
+ def apply_defaults(params)
150
+ # Only apply defaults for keys that aren't present
151
+ DEFAULTS.each do |key, value|
152
+ params[key] = value unless params.key?(key)
153
+ end
154
+
155
+ params
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end