Skip to content
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
/test/dummy/storage/
/test/dummy/tmp/
/tmp/
/wip/
docs/.vitepress/cache
docs/.vitepress/dist
docs/parts/examples/*.md
Expand Down
2 changes: 1 addition & 1 deletion lib/active_agent/providers/_base_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def resolve_prompt
with_exception_handling { api_prompt_execute(api_parameters) }
end

process_prompt_finished(api_response.as_json&.deep_symbolize_keys)
process_prompt_finished(api_response)
end

# Orchestrates the complete embedding request lifecycle.
Expand Down
4 changes: 2 additions & 2 deletions lib/active_agent/providers/anthropic/_types.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# frozen_string_literal: true

require_relative "requests/_types"

require_relative "options"
require_relative "request"

Expand All @@ -11,6 +9,8 @@ module Anthropic
# ActiveModel type for casting and serializing Anthropic Request objects.
#
# Handles conversion between Hash, Request, and serialized formats for API calls.
# The Request class now delegates to the official Anthropic gem model, eliminating
# the need for maintaining nested type definitions.
class RequestType < ActiveModel::Type::Value
# Casts input to Request object.
#
Expand Down
216 changes: 135 additions & 81 deletions lib/active_agent/providers/anthropic/request.rb
Original file line number Diff line number Diff line change
@@ -1,107 +1,161 @@
# frozen_string_literal: true

require "active_agent/providers/common/model"
require_relative "_types"
require "delegate"
require "json"
require_relative "transforms"

module ActiveAgent
module Providers
module Anthropic
class Request < Common::BaseModel
# Required parameters
attribute :model, :string
attribute :messages, Requests::Messages::MessagesType.new
attribute :max_tokens, :integer, fallback: 4096

# Optional parameters - Prompting
attribute :system, Requests::Messages::SystemType.new
attribute :temperature, :float
attribute :top_k, :integer
attribute :top_p, :float
attribute :stop_sequences, default: -> { [] } # Array of strings

# Optional parameters - Tools
attribute :tools # Array of tool definitions
attribute :tool_choice, Requests::ToolChoice::ToolChoiceType.new

# Optional parameters - Thinking
attribute :thinking, Requests::ThinkingConfig::ThinkingConfigType.new

# Optional parameters - Streaming
attribute :stream, :boolean, default: false

# Optional parameters - Metadata
attribute :metadata, Requests::MetadataType.new

# Optional parameters - Context Management
attribute :context_management, Requests::ContextManagementConfigType.new

# Optional parameters - Container
attribute :container, Requests::ContainerParamsType.new

# Optional parameters - Service tier
attribute :service_tier, :string

# Optional parameters - MCP Servers
attribute :mcp_servers, default: -> { [] } # Array of MCP server definitions

# Common Format Compatibility
attribute :response_format, Requests::ResponseFormatType.new

# Validations for required fields
validates :model, :messages, :max_tokens, presence: true
# Request wrapper that delegates to Anthropic gem model.
#
# Uses SimpleDelegator to wrap ::Anthropic::Models::MessageCreateParams,
# eliminating the need to maintain duplicate attribute definitions while
# providing convenience transformations and custom fields.
#
# All standard Anthropic API fields are automatically available via delegation:
# - model, messages, max_tokens
# - system, temperature, top_k, top_p, stop_sequences
# - tools, tool_choice, thinking
# - stream, metadata, context_management, container, service_tier, mcp_servers
#
# Custom fields managed separately:
# - response_format (simulated JSON mode feature)
#
# @example Basic usage
# request = Request.new(
# model: "claude-3-5-haiku-latest",
# messages: [{role: "user", content: "Hello"}]
# )
# request.model #=> "claude-3-5-haiku-latest"
# request.max_tokens #=> 4096 (default)
#
# @example With transformations
# # String content is automatically normalized
# request = Request.new(
# model: "...",
# messages: [{role: "user", content: "Hi"}]
# )
# # Internally becomes: [{type: "text", text: "Hi"}]
#
# @example Custom field
# request = Request.new(
# model: "...",
# messages: [...],
# response_format: {type: "json_object"}
# )
# request.response_format #=> {type: "json_object"}
class Request < SimpleDelegator
# Default max_tokens value when not specified
DEFAULT_MAX_TOKENS = 4096

# Default values for optional parameters
DEFAULTS = {
max_tokens: DEFAULT_MAX_TOKENS,
stop_sequences: [],
mcp_servers: []
}.freeze

# @return [Hash, nil] simulated JSON response format configuration
attr_reader :response_format

# @return [Boolean, nil] whether to stream the response
attr_reader :stream

# @param params [Hash]
# @option params [String] :model required
# @option params [Array<Hash>] :messages required
# @option params [Integer] :max_tokens (4096)
# @option params [Hash] :response_format custom field for JSON mode simulation
# @raise [ArgumentError] when gem model validation fails
def initialize(**params)
# Step 1: Extract custom fields that gem doesn't support
@response_format = params.delete(:response_format)
@stream = params.delete(:stream)

# Step 2: Map common format 'instructions' to Anthropic's 'system'
if params.key?(:instructions)
params[:system] = params.delete(:instructions)
end

# Validations for numeric parameters
validates :max_tokens, numericality: { greater_than_or_equal_to: 1 }, allow_nil: true
validates :temperature, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }, allow_nil: true
validates :top_k, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
validates :top_p, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }, allow_nil: true
# Step 3: Apply defaults
params = apply_defaults(params)

# Validations for specific values
validates :service_tier, inclusion: { in: %w[auto standard_only] }, allow_nil: true
# Step 4: Transform params for gem compatibility
transformed = Transforms.normalize_params(params)

# Custom validations
validate :validate_stop_sequences
validate :validate_tools_format
validate :validate_mcp_servers_format
# Step 5: Create gem model - this validates all parameters!
gem_model = ::Anthropic::Models::MessageCreateParams.new(**transformed)

# Common Format Compatibility
alias_attribute :instructions, :system
# Step 6: Delegate all method calls to gem model
super(gem_model)
rescue ArgumentError => e
# Re-raise with more context
raise ArgumentError, "Invalid Anthropic request parameters: #{e.message}"
end

# Handle merging in the common format
def message=(value)
self.messages ||= []
self.messages << Requests::Messages::MessageType.new.cast(value)
# Serializes request for API call.
#
# Uses gem's JSON serialization and delegates cleanup to Transforms module.
#
# @return [Hash]
def serialize
# Use gem's JSON serialization (handles all nested objects)
hash = Anthropic::Transforms.gem_to_hash(__getobj__)

# Delegate cleanup to transforms module
Transforms.cleanup_serialized_request(hash, DEFAULTS, __getobj__)
end

private
# Accessor for system instructions.
#
# Must override SimpleDelegator's method_missing because Ruby's Kernel.system
# conflicts with delegation. The gem stores data in @data instance variable.
#
# @return [String, Array, nil]
def system
__getobj__.instance_variable_get(:@data)[:system]
end

def validate_stop_sequences
return if stop_sequences.nil? || stop_sequences.empty?
# @param value [String, Array]
def system=(value)
__getobj__.instance_variable_get(:@data)[:system] = value
end

unless stop_sequences.is_a?(Array)
errors.add(:stop_sequences, "must be an array")
end
# Alias for system (common format compatibility).
#
# @return [String, Array, nil]
def instructions
system
end

def validate_tools_format
return if tools.nil?
# @param value [String, Array]
def instructions=(value)
self.system = value
end

unless tools.is_a?(Array)
errors.add(:tools, "must be an array")
end
# Removes the last message from the messages array.
#
# Used for JSON format simulation to remove the lead-in assistant message.
#
# @return [void]
def pop_message!
new_messages = messages.dup
new_messages.pop
self.messages = new_messages
end

def validate_mcp_servers_format
return if mcp_servers.nil? || mcp_servers.empty?
private

unless mcp_servers.is_a?(Array)
errors.add(:mcp_servers, "must be an array")
return
# @param params [Hash]
# @return [Hash]
def apply_defaults(params)
# Only apply defaults for keys that aren't present
DEFAULTS.each do |key, value|
params[key] = value unless params.key?(key)
end

if mcp_servers.length > 20
errors.add(:mcp_servers, "can have at most 20 servers")
end
params
end
end
end
Expand Down
Loading