lib/active_genie/clients/anthropic_client.rb
require 'json' require 'net/http' require 'uri' require_relative './helpers/retry' require_relative './base_client' module ActiveGenie::Clients # Client for interacting with the Anthropic (Claude) API with json response class AnthropicClient < BaseClient class AnthropicError < ClientError; end class RateLimitError < AnthropicError; end ANTHROPIC_VERSION = '2023-06-01' ANTHROPIC_ENDPOINT = '/v1/messages' def initialize(config) super(config) end # Requests structured JSON output from the Anthropic Claude model based on a schema. # # @param messages [Array<Hash>] A list of messages representing the conversation history. # Each hash should have :role ('user', 'assistant', or 'system') and :content (String). # Claude uses 'user', 'assistant', and 'system' roles. # @param function [Hash] A JSON schema definition describing the desired output format. # @param model_tier [Symbol, nil] A symbolic representation of the model quality/size tier. # @param config [Hash] Optional configuration overrides: # - :api_key [String] Override the default API key. # - :model [String] Override the model name directly. # - :max_retries [Integer] Max retries for the request. # - :retry_delay [Integer] Initial delay for retries. # - :anthropic_version [String] Override the default Anthropic API version. # @return [Hash, nil] The parsed JSON object matching the schema, or nil if parsing fails or content is empty. def function_calling(messages, function, model_tier: nil, config: {}) model = config[:runtime][:model] || @app_config.tier_to_model(model_tier) system_message = messages.find { |m| m[:role] == 'system' }&.dig(:content) || '' user_messages = messages.select { |m| m[:role] == 'user' || m[:role] == 'assistant' } .map { |m| { role: m[:role], content: m[:content] } } anthropic_function = function.dup anthropic_function[:input_schema] = function[:parameters] anthropic_function.delete(:parameters) payload = { model:, system: system_message, messages: user_messages, tools: [anthropic_function], tool_choice: { name: anthropic_function[:name], type: 'tool' }, max_tokens: config[:runtime][:max_tokens], temperature: config[:runtime][:temperature] || 0, } api_key = config[:runtime][:api_key] || @app_config.api_key headers = { 'x-api-key': api_key, 'anthropic-version': config[:anthropic_version] || ANTHROPIC_VERSION }.compact retry_with_backoff(config:) do start_time = Time.now response = post(ANTHROPIC_ENDPOINT, payload, headers: headers, config: config) content = response.dig('content', 0, 'input') ActiveGenie::Logger.trace({ code: :llm_usage, input_tokens: response.dig('usage', 'input_tokens'), output_tokens: response.dig('usage', 'output_tokens'), total_tokens: response.dig('usage', 'input_tokens') + response.dig('usage', 'output_tokens'), model: payload[:model], duration: Time.now - start_time, usage: response.dig('usage') }) ActiveGenie::Logger.trace({code: :function_calling, payload:, parsed_response: content}) content end end end end