|
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 commit comments