activeagent 1.0.0.rc1 → 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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/lib/active_agent/providers/_base_provider.rb +92 -82
  4. data/lib/active_agent/providers/anthropic/_types.rb +2 -2
  5. data/lib/active_agent/providers/anthropic/request.rb +135 -81
  6. data/lib/active_agent/providers/anthropic/transforms.rb +353 -0
  7. data/lib/active_agent/providers/anthropic_provider.rb +96 -53
  8. data/lib/active_agent/providers/common/messages/_types.rb +37 -1
  9. data/lib/active_agent/providers/common/responses/base.rb +118 -70
  10. data/lib/active_agent/providers/common/usage.rb +385 -0
  11. data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
  12. data/lib/active_agent/providers/log_subscriber.rb +64 -246
  13. data/lib/active_agent/providers/mock_provider.rb +23 -23
  14. data/lib/active_agent/providers/ollama/chat/request.rb +214 -35
  15. data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
  16. data/lib/active_agent/providers/ollama/embedding/request.rb +160 -47
  17. data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
  18. data/lib/active_agent/providers/ollama_provider.rb +0 -1
  19. data/lib/active_agent/providers/open_ai/_base.rb +3 -2
  20. data/lib/active_agent/providers/open_ai/chat/_types.rb +13 -1
  21. data/lib/active_agent/providers/open_ai/chat/request.rb +132 -186
  22. data/lib/active_agent/providers/open_ai/chat/transforms.rb +364 -0
  23. data/lib/active_agent/providers/open_ai/chat_provider.rb +57 -36
  24. data/lib/active_agent/providers/open_ai/embedding/_types.rb +13 -2
  25. data/lib/active_agent/providers/open_ai/embedding/request.rb +38 -70
  26. data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
  27. data/lib/active_agent/providers/open_ai/responses/_types.rb +1 -7
  28. data/lib/active_agent/providers/open_ai/responses/request.rb +100 -134
  29. data/lib/active_agent/providers/open_ai/responses/transforms.rb +228 -0
  30. data/lib/active_agent/providers/open_ai/responses_provider.rb +77 -30
  31. data/lib/active_agent/providers/open_ai_provider.rb +0 -3
  32. data/lib/active_agent/providers/open_router/_types.rb +27 -1
  33. data/lib/active_agent/providers/open_router/options.rb +49 -1
  34. data/lib/active_agent/providers/open_router/request.rb +232 -66
  35. data/lib/active_agent/providers/open_router/requests/_types.rb +0 -1
  36. data/lib/active_agent/providers/open_router/requests/messages/_types.rb +37 -40
  37. data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +19 -3
  38. data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +15 -4
  39. data/lib/active_agent/providers/open_router/requests/plugin.rb +19 -3
  40. data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +30 -8
  41. data/lib/active_agent/providers/open_router/requests/prediction.rb +17 -0
  42. data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +41 -7
  43. data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +60 -19
  44. data/lib/active_agent/providers/open_router/requests/response_format.rb +30 -2
  45. data/lib/active_agent/providers/open_router/transforms.rb +134 -0
  46. data/lib/active_agent/providers/open_router_provider.rb +9 -0
  47. data/lib/active_agent/version.rb +1 -1
  48. metadata +15 -159
  49. data/lib/active_agent/generation_provider/open_router/types.rb +0 -505
  50. data/lib/active_agent/generation_provider/xai_provider.rb +0 -144
  51. data/lib/active_agent/providers/anthropic/requests/_types.rb +0 -190
  52. data/lib/active_agent/providers/anthropic/requests/container_params.rb +0 -19
  53. data/lib/active_agent/providers/anthropic/requests/content/base.rb +0 -21
  54. data/lib/active_agent/providers/anthropic/requests/content/sources/base.rb +0 -22
  55. data/lib/active_agent/providers/anthropic/requests/context_management_config.rb +0 -18
  56. data/lib/active_agent/providers/anthropic/requests/messages/_types.rb +0 -189
  57. data/lib/active_agent/providers/anthropic/requests/messages/assistant.rb +0 -23
  58. data/lib/active_agent/providers/anthropic/requests/messages/base.rb +0 -63
  59. data/lib/active_agent/providers/anthropic/requests/messages/content/_types.rb +0 -143
  60. data/lib/active_agent/providers/anthropic/requests/messages/content/base.rb +0 -21
  61. data/lib/active_agent/providers/anthropic/requests/messages/content/document.rb +0 -26
  62. data/lib/active_agent/providers/anthropic/requests/messages/content/image.rb +0 -23
  63. data/lib/active_agent/providers/anthropic/requests/messages/content/redacted_thinking.rb +0 -21
  64. data/lib/active_agent/providers/anthropic/requests/messages/content/search_result.rb +0 -27
  65. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/_types.rb +0 -171
  66. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/base.rb +0 -22
  67. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_base64.rb +0 -25
  68. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_file.rb +0 -23
  69. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_text.rb +0 -25
  70. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_url.rb +0 -23
  71. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_base64.rb +0 -27
  72. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_file.rb +0 -23
  73. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_url.rb +0 -23
  74. data/lib/active_agent/providers/anthropic/requests/messages/content/text.rb +0 -22
  75. data/lib/active_agent/providers/anthropic/requests/messages/content/thinking.rb +0 -23
  76. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_result.rb +0 -24
  77. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_use.rb +0 -28
  78. data/lib/active_agent/providers/anthropic/requests/messages/user.rb +0 -21
  79. data/lib/active_agent/providers/anthropic/requests/metadata.rb +0 -18
  80. data/lib/active_agent/providers/anthropic/requests/response_format.rb +0 -22
  81. data/lib/active_agent/providers/anthropic/requests/thinking_config/_types.rb +0 -60
  82. data/lib/active_agent/providers/anthropic/requests/thinking_config/base.rb +0 -20
  83. data/lib/active_agent/providers/anthropic/requests/thinking_config/disabled.rb +0 -16
  84. data/lib/active_agent/providers/anthropic/requests/thinking_config/enabled.rb +0 -20
  85. data/lib/active_agent/providers/anthropic/requests/tool_choice/_types.rb +0 -78
  86. data/lib/active_agent/providers/anthropic/requests/tool_choice/any.rb +0 -17
  87. data/lib/active_agent/providers/anthropic/requests/tool_choice/auto.rb +0 -17
  88. data/lib/active_agent/providers/anthropic/requests/tool_choice/base.rb +0 -20
  89. data/lib/active_agent/providers/anthropic/requests/tool_choice/none.rb +0 -16
  90. data/lib/active_agent/providers/anthropic/requests/tool_choice/tool.rb +0 -20
  91. data/lib/active_agent/providers/ollama/chat/requests/_types.rb +0 -3
  92. data/lib/active_agent/providers/ollama/chat/requests/messages/_types.rb +0 -116
  93. data/lib/active_agent/providers/ollama/chat/requests/messages/assistant.rb +0 -19
  94. data/lib/active_agent/providers/ollama/chat/requests/messages/user.rb +0 -19
  95. data/lib/active_agent/providers/ollama/embedding/requests/_types.rb +0 -83
  96. data/lib/active_agent/providers/ollama/embedding/requests/options.rb +0 -104
  97. data/lib/active_agent/providers/open_ai/chat/requests/_types.rb +0 -229
  98. data/lib/active_agent/providers/open_ai/chat/requests/audio.rb +0 -24
  99. data/lib/active_agent/providers/open_ai/chat/requests/messages/_types.rb +0 -123
  100. data/lib/active_agent/providers/open_ai/chat/requests/messages/assistant.rb +0 -42
  101. data/lib/active_agent/providers/open_ai/chat/requests/messages/base.rb +0 -78
  102. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/_types.rb +0 -133
  103. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/audio.rb +0 -35
  104. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/base.rb +0 -24
  105. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/file.rb +0 -26
  106. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/_types.rb +0 -60
  107. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/details.rb +0 -41
  108. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/image.rb +0 -37
  109. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/refusal.rb +0 -25
  110. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/text.rb +0 -25
  111. data/lib/active_agent/providers/open_ai/chat/requests/messages/developer.rb +0 -25
  112. data/lib/active_agent/providers/open_ai/chat/requests/messages/function.rb +0 -25
  113. data/lib/active_agent/providers/open_ai/chat/requests/messages/system.rb +0 -25
  114. data/lib/active_agent/providers/open_ai/chat/requests/messages/tool.rb +0 -26
  115. data/lib/active_agent/providers/open_ai/chat/requests/messages/user.rb +0 -32
  116. data/lib/active_agent/providers/open_ai/chat/requests/prediction.rb +0 -46
  117. data/lib/active_agent/providers/open_ai/chat/requests/response_format.rb +0 -53
  118. data/lib/active_agent/providers/open_ai/chat/requests/stream_options.rb +0 -24
  119. data/lib/active_agent/providers/open_ai/chat/requests/tool_choice.rb +0 -26
  120. data/lib/active_agent/providers/open_ai/chat/requests/tools/_types.rb +0 -5
  121. data/lib/active_agent/providers/open_ai/chat/requests/tools/base.rb +0 -22
  122. data/lib/active_agent/providers/open_ai/chat/requests/tools/custom_tool.rb +0 -41
  123. data/lib/active_agent/providers/open_ai/chat/requests/tools/function_tool.rb +0 -51
  124. data/lib/active_agent/providers/open_ai/chat/requests/web_search_options.rb +0 -45
  125. data/lib/active_agent/providers/open_ai/embedding/requests/_types.rb +0 -49
  126. data/lib/active_agent/providers/open_ai/responses/requests/_types.rb +0 -231
  127. data/lib/active_agent/providers/open_ai/responses/requests/conversation.rb +0 -23
  128. data/lib/active_agent/providers/open_ai/responses/requests/inputs/_types.rb +0 -264
  129. data/lib/active_agent/providers/open_ai/responses/requests/inputs/assistant_message.rb +0 -22
  130. data/lib/active_agent/providers/open_ai/responses/requests/inputs/base.rb +0 -89
  131. data/lib/active_agent/providers/open_ai/responses/requests/inputs/code_interpreter_tool_call.rb +0 -30
  132. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call.rb +0 -28
  133. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call_output.rb +0 -33
  134. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/_types.rb +0 -207
  135. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/base.rb +0 -22
  136. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_audio.rb +0 -26
  137. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_file.rb +0 -28
  138. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_image.rb +0 -28
  139. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_text.rb +0 -25
  140. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call.rb +0 -28
  141. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call_output.rb +0 -27
  142. data/lib/active_agent/providers/open_ai/responses/requests/inputs/developer_message.rb +0 -20
  143. data/lib/active_agent/providers/open_ai/responses/requests/inputs/file_search_tool_call.rb +0 -25
  144. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_call_output.rb +0 -32
  145. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_tool_call.rb +0 -28
  146. data/lib/active_agent/providers/open_ai/responses/requests/inputs/image_gen_tool_call.rb +0 -27
  147. data/lib/active_agent/providers/open_ai/responses/requests/inputs/input_message.rb +0 -31
  148. data/lib/active_agent/providers/open_ai/responses/requests/inputs/item_reference.rb +0 -23
  149. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call.rb +0 -26
  150. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call_output.rb +0 -33
  151. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_request.rb +0 -30
  152. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_response.rb +0 -28
  153. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_list_tools.rb +0 -29
  154. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_tool_call.rb +0 -35
  155. data/lib/active_agent/providers/open_ai/responses/requests/inputs/output_message.rb +0 -35
  156. data/lib/active_agent/providers/open_ai/responses/requests/inputs/reasoning.rb +0 -33
  157. data/lib/active_agent/providers/open_ai/responses/requests/inputs/system_message.rb +0 -20
  158. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_call_base.rb +0 -27
  159. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_message.rb +0 -23
  160. data/lib/active_agent/providers/open_ai/responses/requests/inputs/user_message.rb +0 -20
  161. data/lib/active_agent/providers/open_ai/responses/requests/inputs/web_search_tool_call.rb +0 -24
  162. data/lib/active_agent/providers/open_ai/responses/requests/prompt_reference.rb +0 -23
  163. data/lib/active_agent/providers/open_ai/responses/requests/reasoning.rb +0 -23
  164. data/lib/active_agent/providers/open_ai/responses/requests/stream_options.rb +0 -20
  165. data/lib/active_agent/providers/open_ai/responses/requests/text/_types.rb +0 -89
  166. data/lib/active_agent/providers/open_ai/responses/requests/text/base.rb +0 -22
  167. data/lib/active_agent/providers/open_ai/responses/requests/text/json_object.rb +0 -20
  168. data/lib/active_agent/providers/open_ai/responses/requests/text/json_schema.rb +0 -48
  169. data/lib/active_agent/providers/open_ai/responses/requests/text/plain.rb +0 -20
  170. data/lib/active_agent/providers/open_ai/responses/requests/text.rb +0 -41
  171. data/lib/active_agent/providers/open_ai/responses/requests/tool_choice.rb +0 -26
  172. data/lib/active_agent/providers/open_ai/responses/requests/tools/_types.rb +0 -112
  173. data/lib/active_agent/providers/open_ai/responses/requests/tools/base.rb +0 -25
  174. data/lib/active_agent/providers/open_ai/responses/requests/tools/code_interpreter_tool.rb +0 -23
  175. data/lib/active_agent/providers/open_ai/responses/requests/tools/computer_tool.rb +0 -27
  176. data/lib/active_agent/providers/open_ai/responses/requests/tools/custom_tool.rb +0 -28
  177. data/lib/active_agent/providers/open_ai/responses/requests/tools/file_search_tool.rb +0 -27
  178. data/lib/active_agent/providers/open_ai/responses/requests/tools/function_tool.rb +0 -29
  179. data/lib/active_agent/providers/open_ai/responses/requests/tools/image_generation_tool.rb +0 -37
  180. data/lib/active_agent/providers/open_ai/responses/requests/tools/local_shell_tool.rb +0 -21
  181. data/lib/active_agent/providers/open_ai/responses/requests/tools/mcp_tool.rb +0 -41
  182. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_preview_tool.rb +0 -24
  183. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_tool.rb +0 -25
  184. data/lib/active_agent/providers/open_ai/schema.yml +0 -65937
  185. data/lib/active_agent/providers/open_router/requests/message.rb +0 -1
  186. data/lib/active_agent/providers/open_router/requests/messages/assistant.rb +0 -20
  187. data/lib/active_agent/providers/open_router/requests/messages/user.rb +0 -30
@@ -1,107 +1,161 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_agent/providers/common/model"
4
- require_relative "_types"
3
+ require "delegate"
4
+ require "json"
5
+ require_relative "transforms"
5
6
 
6
7
  module ActiveAgent
7
8
  module Providers
8
9
  module Anthropic
9
- class Request < Common::BaseModel
10
- # Required parameters
11
- attribute :model, :string
12
- attribute :messages, Requests::Messages::MessagesType.new
13
- attribute :max_tokens, :integer, fallback: 4096
14
-
15
- # Optional parameters - Prompting
16
- attribute :system, Requests::Messages::SystemType.new
17
- attribute :temperature, :float
18
- attribute :top_k, :integer
19
- attribute :top_p, :float
20
- attribute :stop_sequences, default: -> { [] } # Array of strings
21
-
22
- # Optional parameters - Tools
23
- attribute :tools # Array of tool definitions
24
- attribute :tool_choice, Requests::ToolChoice::ToolChoiceType.new
25
-
26
- # Optional parameters - Thinking
27
- attribute :thinking, Requests::ThinkingConfig::ThinkingConfigType.new
28
-
29
- # Optional parameters - Streaming
30
- attribute :stream, :boolean, default: false
31
-
32
- # Optional parameters - Metadata
33
- attribute :metadata, Requests::MetadataType.new
34
-
35
- # Optional parameters - Context Management
36
- attribute :context_management, Requests::ContextManagementConfigType.new
37
-
38
- # Optional parameters - Container
39
- attribute :container, Requests::ContainerParamsType.new
40
-
41
- # Optional parameters - Service tier
42
- attribute :service_tier, :string
43
-
44
- # Optional parameters - MCP Servers
45
- attribute :mcp_servers, default: -> { [] } # Array of MCP server definitions
46
-
47
- # Common Format Compatibility
48
- attribute :response_format, Requests::ResponseFormatType.new
49
-
50
- # Validations for required fields
51
- validates :model, :messages, :max_tokens, presence: true
10
+ # Request wrapper that delegates to Anthropic gem model.
11
+ #
12
+ # Uses SimpleDelegator to wrap ::Anthropic::Models::MessageCreateParams,
13
+ # eliminating the need to maintain duplicate attribute definitions while
14
+ # providing convenience transformations and custom fields.
15
+ #
16
+ # All standard Anthropic API fields are automatically available via delegation:
17
+ # - model, messages, max_tokens
18
+ # - system, temperature, top_k, top_p, stop_sequences
19
+ # - tools, tool_choice, thinking
20
+ # - stream, metadata, context_management, container, service_tier, mcp_servers
21
+ #
22
+ # Custom fields managed separately:
23
+ # - response_format (simulated JSON mode feature)
24
+ #
25
+ # @example Basic usage
26
+ # request = Request.new(
27
+ # model: "claude-3-5-haiku-latest",
28
+ # messages: [{role: "user", content: "Hello"}]
29
+ # )
30
+ # request.model #=> "claude-3-5-haiku-latest"
31
+ # request.max_tokens #=> 4096 (default)
32
+ #
33
+ # @example With transformations
34
+ # # String content is automatically normalized
35
+ # request = Request.new(
36
+ # model: "...",
37
+ # messages: [{role: "user", content: "Hi"}]
38
+ # )
39
+ # # Internally becomes: [{type: "text", text: "Hi"}]
40
+ #
41
+ # @example Custom field
42
+ # request = Request.new(
43
+ # model: "...",
44
+ # messages: [...],
45
+ # response_format: {type: "json_object"}
46
+ # )
47
+ # request.response_format #=> {type: "json_object"}
48
+ class Request < SimpleDelegator
49
+ # Default max_tokens value when not specified
50
+ DEFAULT_MAX_TOKENS = 4096
51
+
52
+ # Default values for optional parameters
53
+ DEFAULTS = {
54
+ max_tokens: DEFAULT_MAX_TOKENS,
55
+ stop_sequences: [],
56
+ mcp_servers: []
57
+ }.freeze
58
+
59
+ # @return [Hash, nil] simulated JSON response format configuration
60
+ attr_reader :response_format
61
+
62
+ # @return [Boolean, nil] whether to stream the response
63
+ attr_reader :stream
64
+
65
+ # @param params [Hash]
66
+ # @option params [String] :model required
67
+ # @option params [Array<Hash>] :messages required
68
+ # @option params [Integer] :max_tokens (4096)
69
+ # @option params [Hash] :response_format custom field for JSON mode simulation
70
+ # @raise [ArgumentError] when gem model validation fails
71
+ def initialize(**params)
72
+ # Step 1: Extract custom fields that gem doesn't support
73
+ @response_format = params.delete(:response_format)
74
+ @stream = params.delete(:stream)
75
+
76
+ # Step 2: Map common format 'instructions' to Anthropic's 'system'
77
+ if params.key?(:instructions)
78
+ params[:system] = params.delete(:instructions)
79
+ end
52
80
 
53
- # Validations for numeric parameters
54
- validates :max_tokens, numericality: { greater_than_or_equal_to: 1 }, allow_nil: true
55
- validates :temperature, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }, allow_nil: true
56
- validates :top_k, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
57
- validates :top_p, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }, allow_nil: true
81
+ # Step 3: Apply defaults
82
+ params = apply_defaults(params)
58
83
 
59
- # Validations for specific values
60
- validates :service_tier, inclusion: { in: %w[auto standard_only] }, allow_nil: true
84
+ # Step 4: Transform params for gem compatibility
85
+ transformed = Transforms.normalize_params(params)
61
86
 
62
- # Custom validations
63
- validate :validate_stop_sequences
64
- validate :validate_tools_format
65
- validate :validate_mcp_servers_format
87
+ # Step 5: Create gem model - this validates all parameters!
88
+ gem_model = ::Anthropic::Models::MessageCreateParams.new(**transformed)
66
89
 
67
- # Common Format Compatibility
68
- alias_attribute :instructions, :system
90
+ # Step 6: Delegate all method calls to gem model
91
+ super(gem_model)
92
+ rescue ArgumentError => e
93
+ # Re-raise with more context
94
+ raise ArgumentError, "Invalid Anthropic request parameters: #{e.message}"
95
+ end
69
96
 
70
- # Handle merging in the common format
71
- def message=(value)
72
- self.messages ||= []
73
- self.messages << Requests::Messages::MessageType.new.cast(value)
97
+ # Serializes request for API call.
98
+ #
99
+ # Uses gem's JSON serialization and delegates cleanup to Transforms module.
100
+ #
101
+ # @return [Hash]
102
+ def serialize
103
+ # Use gem's JSON serialization (handles all nested objects)
104
+ hash = Anthropic::Transforms.gem_to_hash(__getobj__)
105
+
106
+ # Delegate cleanup to transforms module
107
+ Transforms.cleanup_serialized_request(hash, DEFAULTS, __getobj__)
74
108
  end
75
109
 
76
- private
110
+ # Accessor for system instructions.
111
+ #
112
+ # Must override SimpleDelegator's method_missing because Ruby's Kernel.system
113
+ # conflicts with delegation. The gem stores data in @data instance variable.
114
+ #
115
+ # @return [String, Array, nil]
116
+ def system
117
+ __getobj__.instance_variable_get(:@data)[:system]
118
+ end
77
119
 
78
- def validate_stop_sequences
79
- return if stop_sequences.nil? || stop_sequences.empty?
120
+ # @param value [String, Array]
121
+ def system=(value)
122
+ __getobj__.instance_variable_get(:@data)[:system] = value
123
+ end
80
124
 
81
- unless stop_sequences.is_a?(Array)
82
- errors.add(:stop_sequences, "must be an array")
83
- end
125
+ # Alias for system (common format compatibility).
126
+ #
127
+ # @return [String, Array, nil]
128
+ def instructions
129
+ system
84
130
  end
85
131
 
86
- def validate_tools_format
87
- return if tools.nil?
132
+ # @param value [String, Array]
133
+ def instructions=(value)
134
+ self.system = value
135
+ end
88
136
 
89
- unless tools.is_a?(Array)
90
- errors.add(:tools, "must be an array")
91
- end
137
+ # Removes the last message from the messages array.
138
+ #
139
+ # Used for JSON format simulation to remove the lead-in assistant message.
140
+ #
141
+ # @return [void]
142
+ def pop_message!
143
+ new_messages = messages.dup
144
+ new_messages.pop
145
+ self.messages = new_messages
92
146
  end
93
147
 
94
- def validate_mcp_servers_format
95
- return if mcp_servers.nil? || mcp_servers.empty?
148
+ private
96
149
 
97
- unless mcp_servers.is_a?(Array)
98
- errors.add(:mcp_servers, "must be an array")
99
- return
150
+ # @param params [Hash]
151
+ # @return [Hash]
152
+ def apply_defaults(params)
153
+ # Only apply defaults for keys that aren't present
154
+ DEFAULTS.each do |key, value|
155
+ params[key] = value unless params.key?(key)
100
156
  end
101
157
 
102
- if mcp_servers.length > 20
103
- errors.add(:mcp_servers, "can have at most 20 servers")
104
- end
158
+ params
105
159
  end
106
160
  end
107
161
  end
@@ -0,0 +1,353 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/keys"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Anthropic
8
+ # Transforms between convenient input formats and Anthropic API structures.
9
+ #
10
+ # Provides bidirectional transformations:
11
+ # - Expand: shortcuts → API format (string → content blocks, consecutive messages → grouped)
12
+ # - Compress: API format → shortcuts (single content blocks → strings for efficiency)
13
+ module Transforms
14
+ class << self
15
+ # Converts gem model object to hash via JSON round-trip.
16
+ #
17
+ # This ensures proper nested serialization and symbolic keys.
18
+ #
19
+ # @param gem_object [Object] any object responding to .to_json
20
+ # @return [Hash]
21
+ def gem_to_hash(gem_object)
22
+ JSON.parse(gem_object.to_json, symbolize_names: true)
23
+ end
24
+
25
+ # @param params [Hash]
26
+ # @return [Hash]
27
+ def normalize_params(params)
28
+ params = params.dup
29
+ params[:messages] = normalize_messages(params[:messages]) if params[:messages]
30
+ params[:system] = normalize_system(params[:system]) if params[:system]
31
+ params
32
+ end
33
+
34
+ # Merges consecutive same-role messages into single messages with multiple content blocks.
35
+ #
36
+ # Required by Anthropic API - consecutive messages with the same role must be combined.
37
+ #
38
+ # @param messages [Array<Hash>]
39
+ # @return [Array<Hash>]
40
+ def normalize_messages(messages)
41
+ return messages unless messages.is_a?(Array)
42
+
43
+ grouped = []
44
+
45
+ messages.each do |msg|
46
+ msg_hash = msg.is_a?(Hash) ? msg.deep_symbolize_keys : { role: :user, content: msg }
47
+
48
+ # Extract role
49
+ role = msg_hash[:role]&.to_sym || :user
50
+
51
+ # Determine content
52
+ if msg_hash.key?(:content)
53
+ # Has explicit content key
54
+ content = normalize_content(msg_hash[:content])
55
+ elsif msg_hash.key?(:role) && msg_hash.keys.size > 1
56
+ # Has role + other keys (e.g., {role: "assistant", text: "..."})
57
+ # Treat everything except :role as content
58
+ content = normalize_content(msg_hash.except(:role))
59
+ elsif !msg_hash.key?(:role)
60
+ # No role or content - treat entire hash as content
61
+ content = normalize_content(msg_hash)
62
+ else
63
+ # Only has role, no content
64
+ content = []
65
+ end
66
+
67
+ if grouped.empty? || grouped.last[:role] != role
68
+ grouped << { role: role, content: content }
69
+ else
70
+ # Merge content from consecutive same-role messages
71
+ grouped.last[:content] += content
72
+ end
73
+ end
74
+
75
+ grouped
76
+ end
77
+
78
+ # Converts system content shortcuts to API format.
79
+ #
80
+ # Handles string, hash, or array inputs. Strings pass through unchanged
81
+ # since Anthropic accepts both string and structured formats.
82
+ #
83
+ # @param system [String, Array, Hash]
84
+ # @return [String, Array]
85
+ def normalize_system(system)
86
+ case system
87
+ when String
88
+ # Keep strings as-is - Anthropic accepts both string and array
89
+ system
90
+ when Array
91
+ # Normalize array of system blocks
92
+ system.map { |block| normalize_system_block(block) }
93
+ when Hash
94
+ # Single hash becomes array with one block
95
+ [ normalize_system_block(system) ]
96
+ else
97
+ system
98
+ end
99
+ end
100
+
101
+ # @param block [String, Hash]
102
+ # @return [Hash]
103
+ def normalize_system_block(block)
104
+ case block
105
+ when String
106
+ { type: "text", text: block }
107
+ when Hash
108
+ hash = block.deep_symbolize_keys
109
+ # Add type if missing and can be inferred
110
+ hash[:type] ||= "text" if hash[:text]
111
+ hash
112
+ else
113
+ block
114
+ end
115
+ end
116
+
117
+ # Expands content shortcuts into structured content block arrays.
118
+ #
119
+ # Handles multiple input formats:
120
+ # - String → `[{type: "text", text: "..."}]`
121
+ # - Hash with multiple keys → separate blocks per content type
122
+ # - Array → normalized items
123
+ #
124
+ # @param content [String, Array, Hash]
125
+ # @return [Array<Hash>]
126
+ def normalize_content(content)
127
+ case content
128
+ when String
129
+ # String → array with single text block
130
+ [ { type: "text", text: content } ]
131
+ when Array
132
+ # Normalize each item in the array
133
+ content.flat_map { |item| normalize_content_item(item) }
134
+ when Hash
135
+ # Check if hash has multiple content keys (text, image, document)
136
+ # If so, expand into separate content blocks
137
+ hash = content.deep_symbolize_keys
138
+ content_keys = [ :text, :image, :document ]
139
+ found_keys = content_keys & hash.keys
140
+
141
+ if found_keys.size > 1
142
+ # Multiple content types - expand into array
143
+ found_keys.flat_map { |key| normalize_content_item({ key => hash[key] }) }
144
+ else
145
+ # Single content item
146
+ [ normalize_content_item(content) ]
147
+ end
148
+ when nil
149
+ []
150
+ else
151
+ # Pass through other types (might be gem objects already)
152
+ [ content ]
153
+ end
154
+ end
155
+
156
+ # Infers content block type from hash keys or converts string to text block.
157
+ #
158
+ # @param item [String, Hash]
159
+ # @return [Hash]
160
+ def normalize_content_item(item)
161
+ case item
162
+ when String
163
+ { type: "text", text: item }
164
+ when Hash
165
+ hash = item.deep_symbolize_keys
166
+
167
+ # If type is specified, return as-is
168
+ return hash if hash[:type]
169
+
170
+ # Type inference based on keys
171
+ if hash[:text]
172
+ { type: "text" }.merge(hash)
173
+ elsif hash[:image]
174
+ # Normalize image source format
175
+ source = normalize_source(hash[:image])
176
+ { type: "image", source: source }.merge(hash.except(:image))
177
+ elsif hash[:document]
178
+ # Normalize document source format
179
+ source = normalize_source(hash[:document])
180
+ { type: "document", source: source }.merge(hash.except(:document))
181
+ elsif hash[:tool_use_id]
182
+ # Tool result content
183
+ { type: "tool_result" }.merge(hash)
184
+ elsif hash[:id] && hash[:name] && hash[:input]
185
+ # Tool use content
186
+ { type: "tool_use" }.merge(hash)
187
+ else
188
+ # Unknown format - return as-is and let gem validate
189
+ hash
190
+ end
191
+ else
192
+ # Pass through (might be gem object)
193
+ item
194
+ end
195
+ end
196
+
197
+ # Converts image/document source shortcuts to API structure.
198
+ #
199
+ # Handles multiple formats:
200
+ # - Regular URL → `{type: "url", url: "..."}`
201
+ # - Data URI → `{type: "base64", media_type: "...", data: "..."}`
202
+ # - Hash with base64 → `{type: "base64", media_type: "...", data: "..."}`
203
+ #
204
+ # @param source [String, Hash]
205
+ # @return [Hash]
206
+ def normalize_source(source)
207
+ case source
208
+ when String
209
+ # Check if it's a data URI (e.g., "data:image/png;base64,...")
210
+ if source.start_with?("data:")
211
+ parse_data_uri(source)
212
+ else
213
+ # Regular URL → wrap in url source type
214
+ { type: "url", url: source }
215
+ end
216
+ when Hash
217
+ hash = source.deep_symbolize_keys
218
+ # Already has type → return as-is
219
+ return hash if hash[:type]
220
+
221
+ # Has base64 data → add type
222
+ if hash[:data] && hash[:media_type]
223
+ { type: "base64" }.merge(hash)
224
+ else
225
+ # Unknown format → return as-is
226
+ hash
227
+ end
228
+ else
229
+ source
230
+ end
231
+ end
232
+
233
+ # Extracts media type and data from data URI.
234
+ #
235
+ # Expected format: `data:[<media type>][;base64],<data>`
236
+ #
237
+ # @param data_uri [String] e.g., "data:image/png;base64,iVBORw0..."
238
+ # @return [Hash] `{type: "base64", media_type: "...", data: "..."}`
239
+ def parse_data_uri(data_uri)
240
+ # Extract media type and data from data URI
241
+ # Format: data:[<media type>][;base64],<data>
242
+ match = data_uri.match(%r{\Adata:([^;,]+)(?:;base64)?,(.+)\z})
243
+
244
+ if match
245
+ {
246
+ type: "base64",
247
+ media_type: match[1],
248
+ data: match[2]
249
+ }
250
+ else
251
+ # Invalid data URI - return as URL fallback
252
+ { type: "url", url: data_uri }
253
+ end
254
+ end
255
+
256
+ # Converts single-element content arrays back to string shorthand.
257
+ #
258
+ # Reduces payload size by reversing the expansion done by normalize methods.
259
+ #
260
+ # @param hash [Hash]
261
+ # @return [Hash]
262
+ def compress_content(hash)
263
+ return hash unless hash.is_a?(Hash)
264
+
265
+ # Compress message content
266
+ hash[:messages]&.each do |msg|
267
+ compress_message_content!(msg)
268
+ end
269
+
270
+ # Compress system content
271
+ if hash[:system].is_a?(Array)
272
+ hash[:system] = compress_system_content(hash[:system])
273
+ end
274
+
275
+ hash
276
+ end
277
+
278
+ # Converts single text block arrays to string shorthand.
279
+ #
280
+ # `[{type: "text", text: "hello"}]` → `"hello"`
281
+ #
282
+ # @param msg [Hash] message with :content key
283
+ # @return [void]
284
+ def compress_message_content!(msg)
285
+ content = msg[:content]
286
+ return unless content.is_a?(Array)
287
+
288
+ # Single text block → string shorthand
289
+ if content.one? && content.first.is_a?(Hash) && content.first[:type] == "text"
290
+ msg[:content] = content.first[:text]
291
+ end
292
+ end
293
+
294
+ # Cleans up serialized request for API submission.
295
+ #
296
+ # Removes response-only fields, applies content compression,
297
+ # removes provider-internal fields, and removes default values.
298
+ # Note: max_tokens is kept even if it matches default as Anthropic API requires it.
299
+ #
300
+ # @param hash [Hash] serialized request
301
+ # @param defaults [Hash] default values to remove
302
+ # @param gem_object [Object] original gem object (unused but for consistency)
303
+ # @return [Hash] cleaned request hash
304
+ def cleanup_serialized_request(hash, defaults, gem_object = nil)
305
+ # Remove response-only fields from messages
306
+ if hash[:messages]
307
+ hash[:messages].each do |msg|
308
+ msg.delete(:id)
309
+ msg.delete(:model)
310
+ msg.delete(:stop_reason)
311
+ msg.delete(:stop_sequence)
312
+ msg.delete(:type)
313
+ msg.delete(:usage)
314
+ end
315
+ end
316
+
317
+ # Apply content compression for API efficiency
318
+ compress_content(hash)
319
+
320
+ # Remove provider-internal fields that should not be in API request
321
+ hash.delete(:mcp_servers) # Provider-level config, not API param
322
+ hash.delete(:stop_sequences) if hash[:stop_sequences] == []
323
+
324
+ # Remove default values (except max_tokens which is required by API)
325
+ defaults.each do |key, value|
326
+ next if key == :max_tokens # Anthropic API requires max_tokens
327
+ hash.delete(key) if hash[key] == value
328
+ end
329
+
330
+ hash
331
+ end
332
+
333
+ private
334
+
335
+ # Converts single text block to string.
336
+ #
337
+ # @param system [Array]
338
+ # @return [String, Array]
339
+ def compress_system_content(system)
340
+ return system unless system.is_a?(Array)
341
+
342
+ # Single text block → string shorthand
343
+ if system.one? && system.first.is_a?(Hash) && system.first[:type] == "text"
344
+ system.first[:text]
345
+ else
346
+ system
347
+ end
348
+ end
349
+ end
350
+ end
351
+ end
352
+ end
353
+ end