From 589580e90c8aec9b8902cc8a123fa5ae5a94496c Mon Sep 17 00:00:00 2001 From: Reion19 Date: Wed, 11 Jun 2025 14:46:55 +0300 Subject: [PATCH 1/4] dependencies -> updated --- Gemfile.lock | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 513c283..991b400 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,47 +1,47 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) - bigdecimal (3.1.6) - connection_pool (2.4.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + bigdecimal (3.2.2) + connection_pool (2.5.3) crack (1.0.0) bigdecimal rexml - diff-lcs (1.5.1) - faraday (2.12.0) - faraday-net_http (>= 2.0, < 3.4) + diff-lcs (1.6.2) + faraday (2.13.1) + faraday-net_http (>= 2.0, < 3.5) json logger - faraday-net_http (3.3.0) - net-http + faraday-net_http (3.4.0) + net-http (>= 0.5.0) faraday-net_http_persistent (2.3.0) faraday (~> 2.5) net-http-persistent (>= 4.0.4, < 5) - hashdiff (1.1.0) - json (2.7.2) - logger (1.6.1) - net-http (0.4.1) + hashdiff (1.2.0) + json (2.12.2) + logger (1.7.0) + net-http (0.6.0) uri - net-http-persistent (4.0.4) - connection_pool (~> 2.2) - public_suffix (5.0.4) - rexml (3.2.6) - rspec (3.13.0) + net-http-persistent (4.0.6) + connection_pool (~> 2.2, >= 2.2.4) + public_suffix (6.0.2) + rexml (3.4.1) + rspec (3.13.1) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.0) + rspec-core (3.13.4) rspec-support (~> 3.13.0) - rspec-expectations (3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.0) + rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-support (3.13.0) - uri (0.13.1) - webmock (3.22.0) + rspec-support (3.13.4) + uri (1.0.3) + webmock (3.25.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) From 003af808ed2f3e9df248f6461ad0ef794dd9576e Mon Sep 17 00:00:00 2001 From: Reion19 Date: Wed, 11 Jun 2025 15:52:57 +0300 Subject: [PATCH 2/4] Added .models .update and .chat methods and specs for them --- lib/rubyai.rb | 12 ++++++++++ spec/{ => rubyai}/client_spec.rb | 2 +- spec/{ => rubyai}/configuration_spec.rb | 2 +- spec/rubyai/rubyai_spec.rb | 31 +++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 2 deletions(-) rename spec/{ => rubyai}/client_spec.rb (94%) rename spec/{ => rubyai}/configuration_spec.rb (95%) create mode 100644 spec/rubyai/rubyai_spec.rb diff --git a/lib/rubyai.rb b/lib/rubyai.rb index 211a3bc..2850fc7 100644 --- a/lib/rubyai.rb +++ b/lib/rubyai.rb @@ -9,4 +9,16 @@ module RubyAI class Error < StandardError; end + + def self.models + Configuration::MODELS.keys + end + + def self.chat(config_hash = {}) + Client.new(config_hash).call + end + + def self.configure + yield(Configuration.new) if block_given? + end end diff --git a/spec/client_spec.rb b/spec/rubyai/client_spec.rb similarity index 94% rename from spec/client_spec.rb rename to spec/rubyai/client_spec.rb index f74c472..aadfc86 100644 --- a/spec/client_spec.rb +++ b/spec/rubyai/client_spec.rb @@ -1,5 +1,5 @@ require 'webmock/rspec' -require_relative '../lib/rubyai/client.rb' +require_relative '../../lib/rubyai/client.rb' RSpec.describe RubyAI::Client do let(:api_key) { 'your_api_key' } diff --git a/spec/configuration_spec.rb b/spec/rubyai/configuration_spec.rb similarity index 95% rename from spec/configuration_spec.rb rename to spec/rubyai/configuration_spec.rb index e9298f1..caf3d58 100644 --- a/spec/configuration_spec.rb +++ b/spec/rubyai/configuration_spec.rb @@ -1,5 +1,5 @@ require 'webmock/rspec' -require_relative '../lib/rubyai/client.rb' +require_relative '../../lib/rubyai/client.rb' RSpec.describe RubyAI::Client do let(:api_key) { 'your_api_key' } diff --git a/spec/rubyai/rubyai_spec.rb b/spec/rubyai/rubyai_spec.rb new file mode 100644 index 0000000..54ee4a0 --- /dev/null +++ b/spec/rubyai/rubyai_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../lib/rubyai' + +RSpec.describe RubyAI do + + describe '.models' do + it 'should return available models' do + expect(RubyAI.models).to eq(RubyAI::Configuration::MODELS.keys) + end + end + + describe '.chat' do + let(:api_key) { 'your_api_key' } + let(:messages) { 'Hello, how are you?' } + let(:temperature) { 0.7 } + let(:model) { 'gpt-3.5-turbo' } + let(:client) { described_class.chat(api_key: api_key, messages: messages, temperature: temperature, model: model) } + + + let(:response_body) { { 'completion' => 'This is a response from the model.' } } + let(:status) { 200 } + + before do + stub_request(:post, RubyAI::Configuration::BASE_URL) + .to_return(status: status, body: response_body.to_json, headers: { 'Content-Type' => 'application/json' }) + end + + it 'returns parsed JSON response when passing through client directly' do + expect(described_class.chat).to eq(response_body) + end + end +end \ No newline at end of file From 7153f1a585eb50fd861fee2ba2cfac947730240f Mon Sep 17 00:00:00 2001 From: Reion19 Date: Thu, 12 Jun 2025 19:19:23 +0300 Subject: [PATCH 3/4] Add multi-AI provider support & update config handling --- lib/rubyai.rb | 16 +- lib/rubyai/chat.rb | 41 +++++ lib/rubyai/client.rb | 10 +- lib/rubyai/configuration.rb | 39 ++-- lib/rubyai/http.rb | 46 ++++- lib/rubyai/provider.rb | 15 ++ lib/rubyai/providers/anthropic.rb | 17 ++ lib/rubyai/providers/openai.rb | 20 ++ spec/rubyai/chat_spec.rb | 2 + spec/rubyai/client_spec.rb | 3 +- spec/rubyai/configuration_spec.rb | 36 +++- spec/rubyai/http_spec.rb | 235 ++++++++++++++++++++++++ spec/rubyai/provider_spec.rb | 16 ++ spec/rubyai/providers/anthropic_spec.rb | 15 ++ spec/rubyai/providers/openai_spec.rb | 16 ++ spec/rubyai/rubyai_spec.rb | 25 ++- 16 files changed, 518 insertions(+), 34 deletions(-) create mode 100644 lib/rubyai/chat.rb create mode 100644 lib/rubyai/provider.rb create mode 100644 lib/rubyai/providers/anthropic.rb create mode 100644 lib/rubyai/providers/openai.rb create mode 100644 spec/rubyai/chat_spec.rb create mode 100644 spec/rubyai/http_spec.rb create mode 100644 spec/rubyai/provider_spec.rb create mode 100644 spec/rubyai/providers/anthropic_spec.rb create mode 100644 spec/rubyai/providers/openai_spec.rb diff --git a/lib/rubyai.rb b/lib/rubyai.rb index 2850fc7..5a9d381 100644 --- a/lib/rubyai.rb +++ b/lib/rubyai.rb @@ -2,23 +2,31 @@ require 'faraday/net_http_persistent' require 'json' +require_relative "rubyai/providers/openai" +require_relative "rubyai/providers/anthropic" +require_relative "rubyai/provider" require_relative "rubyai/client" require_relative "rubyai/configuration" require_relative "rubyai/http" +require_relative "rubyai/chat" require_relative "rubyai/version" module RubyAI class Error < StandardError; end def self.models - Configuration::MODELS.keys + Configuration::MODELS end - def self.chat(config_hash = {}) - Client.new(config_hash).call + def self.chat(config = {}) + Client.new(config).call end def self.configure - yield(Configuration.new) if block_given? + yield config + end + + def self.config(config = {}) + @config ||= Configuration.new(config) end end diff --git a/lib/rubyai/chat.rb b/lib/rubyai/chat.rb new file mode 100644 index 0000000..e86688f --- /dev/null +++ b/lib/rubyai/chat.rb @@ -0,0 +1,41 @@ +module RubyAI + class Chat + attr_accessor :provider, :model, :temperature + + def initialize(provider, model: nil, temperature: 0.7) + @provider = provider + @model = model || RubyAI::Configuration::DEFAULT_MODEL + @temperature = temperature + end + + def call(messages) + raise ArgumentError, "Messages cannot be empty" if messages.nil? || messages.empty? + + body = HTTP.build_body(messages, @provider, @model, @temperature) + headers = HTTP.build_headers(provider, RubyAI.config) + + response = connection.post do |req| + req.url Configuration::PROVIDERS[@provider] || Configuration::BASE_URL + req.headers.merge!(headers) + req.body = body.to_json + end + + JSON.parse(response.body) + end + + private + + def connection + @connection ||= Faraday.new do |faraday| + faraday.adapter Faraday.default_adapter + faraday.headers['Content-Type'] = 'application/json' + end + rescue Faraday::Error => e + raise "Connection error: #{e.message}" + rescue JSON::ParserError => e + raise "Response parsing error: #{e.message}" + rescue StandardError => e + raise "An unexpected error occurred: #{e.message}" + end + end +end \ No newline at end of file diff --git a/lib/rubyai/client.rb b/lib/rubyai/client.rb index 6cbc096..c9fe4b6 100644 --- a/lib/rubyai/client.rb +++ b/lib/rubyai/client.rb @@ -2,15 +2,15 @@ module RubyAI class Client attr_reader :configuration - def initialize(config_hash = {}) - @configuration = Configuration.new(config_hash) + def initialize(config = {}) + @configuration ||= RubyAI.config(config) end def call response = connection.post do |req| - req.url Configuration::BASE_URL - req.headers.merge!(HTTP.build_headers(configuration.api_key)) - req.body = HTTP.build_body(configuration.messages, configuration.model, configuration.temperature).to_json + req.url Configuration::PROVIDERS[configuration.provider] || Configuration::BASE_URL + req.headers = (HTTP.build_headers(configuration.provider || RubyAI::Configuration::DEFAULT_PROVIDER, RubyAI.config )) + req.body = HTTP.build_body(configuration.messages, configuration.provider, configuration.model, configuration.temperature).to_json end JSON.parse(response.body) diff --git a/lib/rubyai/configuration.rb b/lib/rubyai/configuration.rb index ed0bc1b..9c0ad27 100644 --- a/lib/rubyai/configuration.rb +++ b/lib/rubyai/configuration.rb @@ -1,31 +1,44 @@ module RubyAI class Configuration - BASE_URL = "https://api.openai.com/v1/chat/completions" + PROVIDERS = { + 'openai' => "https://api.openai.com/v1/chat/completions", + 'anthropic' => "https://api.anthropic.com/v1/chat/completions" + }.freeze + + MODELS = PROVIDERS.to_h do |provider, _url| + [provider, Provider[provider].models] + end.freeze - MODELS = { - "gpt-4" => "gpt-4", - "gpt-4-32k" => "gpt-4-32k", - "gpt-4-turbo" => "gpt-4-turbo", - "gpt-4o-mini" => "gpt-4o-mini", - "o1-mini" => "o1-mini", - "o1-preview" => "o1-preview", - "text-davinci-003" => "text-davinci-003" - } + + BASE_URL = "https://api.openai.com/v1/chat/completions" DEFAULT_MODEL = "gpt-3.5-turbo" - attr_accessor :api_key, :model, :messages, :temperature + DEFAULT_PROVIDER = 'openai' + + # default values for configuration + attr_accessor :api_key, + :model, + :messages, + :temperature, + :provider, + # :providers + :provider, + :openai_api_key, + :anthropic_api_key def initialize(config = {}) - @api_key = config[:api_key] + @api_key = config.fetch(:api_key, openai_api_key) + @openai_api_key = config.fetch(:openai_api_key, api_key) @model = config.fetch(:model, DEFAULT_MODEL) @messages = config.fetch(:messages, nil) @temperature = config.fetch(:temperature, 0.7) + @provider = config.fetch(:provider, "openai") end end def self.configuration - @configuration ||= Configuration.new + @configuration ||= RubyAI.config(config = {}) end def self.configure diff --git a/lib/rubyai/http.rb b/lib/rubyai/http.rb index cd73806..aaf38e5 100644 --- a/lib/rubyai/http.rb +++ b/lib/rubyai/http.rb @@ -2,19 +2,49 @@ module RubyAI module HTTP extend self - def build_body(messages, model, temperature) - { - 'model': Configuration::MODELS[model], + def build_body(messages, provider, model, temperature) + case provider + when 'openai' + { + 'model': Configuration::MODELS[provider][model], 'messages': [{ "role": "user", "content": messages }], 'temperature': temperature - } + } + when 'anthropic' + { + 'model' => Configuration::MODELS[provider][model], + 'max_tokens' => 1024, # Required parameter for Anthropic API + 'messages' => format_messages_for_antropic(messages), + 'temperature' => temperature + } + end end - def build_headers(api_key) - { + def build_headers(provider, config) + case provider + when 'openai' + { 'Content-Type': 'application/json', - 'Authorization': "Bearer #{api_key}" - } + 'Authorization': "Bearer #{config.openai_api_key}" + } + when 'anthropic' + { + 'x-api-key' => config.anthropic_api_key, + 'anthropic-version' => '2023-06-01' + } + end + end + + private + + def format_messages_for_antropic(messages) + # Messages should be an array of message objects + # Each message needs 'role' (either 'user' or 'assistant') and 'content' + if messages.is_a?(String) + [{ 'role' => 'user', 'content' => messages }] + else + messages + end end end end diff --git a/lib/rubyai/provider.rb b/lib/rubyai/provider.rb new file mode 100644 index 0000000..8d52466 --- /dev/null +++ b/lib/rubyai/provider.rb @@ -0,0 +1,15 @@ +module RubyAI + module Provider + PROVIDERS = { + 'openai' => RubyAI::Providers::OpenAI, + # doesn't tested yet because i don't have an anthropic api key + 'anthropic' => RubyAI::Providers::Anthropic + } + + module_function + + def [](provider) + PROVIDERS.fetch(provider) + end + end +end \ No newline at end of file diff --git a/lib/rubyai/providers/anthropic.rb b/lib/rubyai/providers/anthropic.rb new file mode 100644 index 0000000..530c841 --- /dev/null +++ b/lib/rubyai/providers/anthropic.rb @@ -0,0 +1,17 @@ +module RubyAI + module Providers + # doesn't tested yet because i don't have an anthropic api key + class Anthropic + def self.models = { + "claude-2" => "claude-2", + "claude-instant-100k" => "claude-instant-100k", + "claude-1" => "claude-1", + "claude-1.3" => "claude-1.3", + "claude-1.3-sonnet" => "claude-1.3-sonnet", + "claude-1.3-sonnet-100k" => "claude-1.3-sonnet-100k" + }.freeze + end + + # todo: configuration of separate models + end +end \ No newline at end of file diff --git a/lib/rubyai/providers/openai.rb b/lib/rubyai/providers/openai.rb new file mode 100644 index 0000000..14f81e8 --- /dev/null +++ b/lib/rubyai/providers/openai.rb @@ -0,0 +1,20 @@ +module RubyAI + module Providers + class OpenAI + DEFAULT_MODEL = "gpt-3.5-turbo".freeze + + def self.models + {"gpt-3.5-turbo" => "gpt-3.5-turbo", + "gpt-4" => "gpt-4", + "gpt-4-32k" => "gpt-4-32k", + "gpt-4-turbo" => "gpt-4-turbo", + "gpt-4o-mini" => "gpt-4o-mini", + "o1-mini" => "o1-mini", + "o1-preview" => "o1-preview", + "text-davinci-003" => "text-davinci-003" } + end + + # todo: configuration of separate models + end + end +end \ No newline at end of file diff --git a/spec/rubyai/chat_spec.rb b/spec/rubyai/chat_spec.rb new file mode 100644 index 0000000..fc13386 --- /dev/null +++ b/spec/rubyai/chat_spec.rb @@ -0,0 +1,2 @@ +require_relative '../../lib/rubyai/chat' +require 'webmock/rspec' diff --git a/spec/rubyai/client_spec.rb b/spec/rubyai/client_spec.rb index aadfc86..dfe3f03 100644 --- a/spec/rubyai/client_spec.rb +++ b/spec/rubyai/client_spec.rb @@ -6,7 +6,8 @@ let(:messages) { 'Hello, how are you?' } let(:temperature) { 0.7 } let(:model) { 'gpt-3.5-turbo' } - let(:client) { described_class.new(api_key: api_key, messages: messages, temperature: temperature, model: model) } + let(:provider) { 'openai' } + let(:client) { described_class.new(api_key: api_key, messages: messages, temperature: temperature, provider: provider, model: model) } describe '#call' do let(:response_body) { { 'completion' => 'This is a response from the model.' } } diff --git a/spec/rubyai/configuration_spec.rb b/spec/rubyai/configuration_spec.rb index caf3d58..81e8fe1 100644 --- a/spec/rubyai/configuration_spec.rb +++ b/spec/rubyai/configuration_spec.rb @@ -1,14 +1,23 @@ require 'webmock/rspec' -require_relative '../../lib/rubyai/client.rb' +require_relative '../../lib/rubyai/client' +require_relative '../../lib/rubyai/providers/openai' +require_relative '../../lib/rubyai/providers/anthropic' +require_relative '../../lib/rubyai/provider' +require_relative '../../lib/rubyai/configuration' + RSpec.describe RubyAI::Client do let(:api_key) { 'your_api_key' } let(:messages) { 'Hello, how are you?' } let(:temperature) { 0.7 } let(:model) { 'gpt-3.5-turbo' } + let(:provider) { 'openai' } before do RubyAI.configure do |config| + config.provider = provider + config.model = model + config.temperature = temperature config.api_key = api_key config.messages = messages end @@ -30,4 +39,29 @@ expect(result.dig('choices', 0, 'message', 'content')).to eq('This is a response from the model.') end end + + describe 'Constants' do + specify "PROVIDERS" do + expect(RubyAI::Configuration::PROVIDERS).to eq( + 'openai' => "https://api.openai.com/v1/chat/completions", + 'anthropic' => "https://api.anthropic.com/v1/chat/completions" + ) + end + + specify "MODELS should return Hash" do + expect(RubyAI::Configuration::MODELS).to be_an_instance_of(Hash) + end + + specify "BASE_URL" do + expect(RubyAI::Configuration::BASE_URL).to eq("https://api.openai.com/v1/chat/completions") + end + + specify "DEFAULT_MODEL" do + expect(RubyAI::Configuration::DEFAULT_MODEL).to eq("gpt-3.5-turbo") + end + + specify "DEFAULT_PROVIDER" do + expect(RubyAI::Configuration::DEFAULT_PROVIDER).to eq("openai") + end + end end diff --git a/spec/rubyai/http_spec.rb b/spec/rubyai/http_spec.rb new file mode 100644 index 0000000..cc9b045 --- /dev/null +++ b/spec/rubyai/http_spec.rb @@ -0,0 +1,235 @@ +require_relative '../../lib/rubyai/http' + +RSpec.describe RubyAI::HTTP do + let(:config) do + double('config', + openai_api_key: 'test-openai-key', + anthropic_api_key: 'test-anthropic-key' + ) + end + + let(:messages) { "Hello, how are you?" } + let(:temperature) { 0.7 } + + # Mock the Configuration::MODELS constant + before do + stub_const('RubyAI::Configuration::MODELS', { + 'openai' => { + 'gpt-3.5-turbo' => 'gpt-3.5-turbo', + 'gpt-4' => 'gpt-4' + }, + 'anthropic' => { + 'claude-3-sonnet' => 'claude-3-sonnet-20240229', + 'claude-3-haiku' => 'claude-3-haiku-20240307' + } + }) + end + + describe '.build_body' do + context 'when provider is openai' do + let(:provider) { 'openai' } + let(:model) { 'gpt-3.5-turbo' } + + it 'returns correct body structure for OpenAI' do + result = described_class.build_body(messages, provider, model, temperature) + + expect(result).to eq({ + :'model' => 'gpt-3.5-turbo', + :'messages' => [{ :"role" => "user", :"content" => messages }], + :'temperature' => temperature + }) + end + + it 'uses the correct model mapping from configuration' do + model = 'gpt-4' + result = described_class.build_body(messages, provider, model, temperature) + + expect(result[:model]).to eq('gpt-4') + end + + it 'includes temperature parameter' do + custom_temp = 0.9 + result = described_class.build_body(messages, provider, model, custom_temp) + + expect(result[:temperature]).to eq(custom_temp) + end + end + + context 'when provider is anthropic' do + let(:provider) { 'anthropic' } + let(:model) { 'claude-3-sonnet' } + + it 'returns correct body structure for Anthropic' do + result = described_class.build_body(messages, provider, model, temperature) + + expect(result).to eq({ + 'model' => 'claude-3-sonnet-20240229', + 'max_tokens' => 1024, + 'messages' => [{ 'role' => 'user', 'content' => messages }], + 'temperature' => temperature + }) + end + + it 'uses the correct model mapping from configuration' do + model = 'claude-3-haiku' + result = described_class.build_body(messages, provider, model, temperature) + + expect(result['model']).to eq('claude-3-haiku-20240307') + end + + it 'includes required max_tokens parameter' do + result = described_class.build_body(messages, provider, model, temperature) + + expect(result['max_tokens']).to eq(1024) + end + + it 'formats messages correctly for single string input' do + result = described_class.build_body(messages, provider, model, temperature) + + expect(result['messages']).to eq([{ 'role' => 'user', 'content' => messages }]) + end + + it 'passes through array messages without modification' do + array_messages = [ + { 'role' => 'user', 'content' => 'Hello' }, + { 'role' => 'assistant', 'content' => 'Hi there!' } + ] + + result = described_class.build_body(array_messages, provider, model, temperature) + + expect(result['messages']).to eq(array_messages) + end + end + + context 'when provider is unsupported' do + it 'returns nil for unsupported provider' do + result = described_class.build_body(messages, 'unsupported', 'model', temperature) + + expect(result).to be_nil + end + end + end + + describe '.build_headers' do + context 'when provider is openai' do + let(:provider) { 'openai' } + + it 'returns correct headers for OpenAI' do + result = described_class.build_headers(provider, config) + + expect(result).to eq({ + 'Content-Type': 'application/json', + 'Authorization': "Bearer #{config.openai_api_key}" + }) + end + + it 'includes the API key in Authorization header' do + result = described_class.build_headers(provider, config) + + expect(result[:'Authorization']).to eq("Bearer test-openai-key") + end + end + + context 'when provider is anthropic' do + let(:provider) { 'anthropic' } + + it 'returns correct headers for Anthropic' do + result = described_class.build_headers(provider, config) + + expect(result).to eq({ + 'x-api-key' => config.anthropic_api_key, + 'anthropic-version' => '2023-06-01' + }) + end + + it 'includes the API key in x-api-key header' do + result = described_class.build_headers(provider, config) + + expect(result['x-api-key']).to eq('test-anthropic-key') + end + + it 'includes the correct anthropic-version' do + result = described_class.build_headers(provider, config) + + expect(result['anthropic-version']).to eq('2023-06-01') + end + end + + context 'when provider is unsupported' do + it 'returns nil for unsupported provider' do + result = described_class.build_headers('unsupported', config) + + expect(result).to be_nil + end + end + end + + describe '.format_messages_for_antropic' do + context 'when messages is a string' do + it 'converts string to proper message format' do + string_message = "Hello world" + result = described_class.send(:format_messages_for_antropic, string_message) + + expect(result).to eq([{ 'role' => 'user', 'content' => string_message }]) + end + end + + context 'when messages is already an array' do + it 'returns the array unchanged' do + array_messages = [ + { 'role' => 'user', 'content' => 'Question' }, + { 'role' => 'assistant', 'content' => 'Answer' } + ] + + result = described_class.send(:format_messages_for_antropic, array_messages) + + expect(result).to eq(array_messages) + end + end + + context 'when messages is empty string' do + it 'handles empty string correctly' do + result = described_class.send(:format_messages_for_antropic, "") + + expect(result).to eq([{ 'role' => 'user', 'content' => "" }]) + end + end + + context 'when messages is nil' do + it 'returns nil unchanged' do + result = described_class.send(:format_messages_for_antropic, nil) + + expect(result).to be_nil + end + end + end + + describe 'integration scenarios' do + it 'builds complete OpenAI request components' do + provider = 'openai' + model = 'gpt-4' + + body = described_class.build_body(messages, provider, model, temperature) + headers = described_class.build_headers(provider, config) + + expect(body[:model]).to eq('gpt-4') + expect(body[:messages]).to be_an(Array) + expect(headers[:'Content-Type']).to eq('application/json') + expect(headers[:'Authorization']).to include('Bearer') + end + + it 'builds complete Anthropic request components' do + provider = 'anthropic' + model = 'claude-3-sonnet' + + body = described_class.build_body(messages, provider, model, temperature) + headers = described_class.build_headers(provider, config) + + expect(body['model']).to eq('claude-3-sonnet-20240229') + expect(body['messages']).to be_an(Array) + expect(body['max_tokens']).to eq(1024) + expect(headers['x-api-key']).to eq('test-anthropic-key') + expect(headers['anthropic-version']).to eq('2023-06-01') + end + end +end \ No newline at end of file diff --git a/spec/rubyai/provider_spec.rb b/spec/rubyai/provider_spec.rb new file mode 100644 index 0000000..69d3321 --- /dev/null +++ b/spec/rubyai/provider_spec.rb @@ -0,0 +1,16 @@ +require 'webmock/rspec' +require_relative '../../lib/rubyai/providers/openai.rb' +require_relative '../../lib/rubyai/providers/anthropic.rb' +require_relative '../../lib/rubyai/provider.rb' + +RSpec.describe RubyAI::Provider do + describe '[]' do + it 'should return the OpenAI provider when "openai" is passed' do + expect(RubyAI::Provider['openai']).to eq(RubyAI::Providers::OpenAI) + end + + it 'should return the Anthropic provider when "anthropic" is passed' do + expect(RubyAI::Provider['anthropic']).to eq(RubyAI::Providers::Anthropic) + end + end +end \ No newline at end of file diff --git a/spec/rubyai/providers/anthropic_spec.rb b/spec/rubyai/providers/anthropic_spec.rb new file mode 100644 index 0000000..0c37ef0 --- /dev/null +++ b/spec/rubyai/providers/anthropic_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../../lib/rubyai/providers/anthropic' + +RSpec.describe RubyAI::Providers::Anthropic do + describe '.models' do + it 'should return an list of models' do + expect(described_class.models).to eq( "claude-2" => "claude-2", + "claude-instant-100k" => "claude-instant-100k", + "claude-1" => "claude-1", + "claude-1.3" => "claude-1.3", + "claude-1.3-sonnet" => "claude-1.3-sonnet", + "claude-1.3-sonnet-100k" => "claude-1.3-sonnet-100k" + ) + end + end +end \ No newline at end of file diff --git a/spec/rubyai/providers/openai_spec.rb b/spec/rubyai/providers/openai_spec.rb new file mode 100644 index 0000000..636d80a --- /dev/null +++ b/spec/rubyai/providers/openai_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../../lib/rubyai/providers/openai' + +RSpec.describe RubyAI::Providers::OpenAI do + describe '.models' do + it 'should return an list of models' do + expect(described_class.models).to eq("gpt-3.5-turbo" => "gpt-3.5-turbo", + "gpt-4" => "gpt-4", + "gpt-4-32k" => "gpt-4-32k", + "gpt-4-turbo" => "gpt-4-turbo", + "gpt-4o-mini" => "gpt-4o-mini", + "o1-mini" => "o1-mini", + "o1-preview" => "o1-preview", + "text-davinci-003" => "text-davinci-003") + end + end +end \ No newline at end of file diff --git a/spec/rubyai/rubyai_spec.rb b/spec/rubyai/rubyai_spec.rb index 54ee4a0..bdcd471 100644 --- a/spec/rubyai/rubyai_spec.rb +++ b/spec/rubyai/rubyai_spec.rb @@ -4,7 +4,7 @@ describe '.models' do it 'should return available models' do - expect(RubyAI.models).to eq(RubyAI::Configuration::MODELS.keys) + expect(RubyAI.models).to eq(RubyAI::Configuration::MODELS) end end @@ -13,7 +13,8 @@ let(:messages) { 'Hello, how are you?' } let(:temperature) { 0.7 } let(:model) { 'gpt-3.5-turbo' } - let(:client) { described_class.chat(api_key: api_key, messages: messages, temperature: temperature, model: model) } + let(:provider) { 'openai' } + let(:client) { described_class.chat(api_key: api_key, messages: messages, temperature: temperature, provider: provider, model: model) } let(:response_body) { { 'completion' => 'This is a response from the model.' } } @@ -28,4 +29,24 @@ expect(described_class.chat).to eq(response_body) end end + + describe '.configure' do + let(:configuration) {RubyAI.config} + + it 'allows configuration of the client' do + described_class.configure do |config| + config.api_key = 'your_api_key' + config.messages = 'Hello, how are you?' + config.temperature = 0.7 + config.provider = 'openai' + config.model = 'gpt-3.5-turbo' + end + + expect(configuration.api_key).to eq('your_api_key') + expect(configuration.messages).to eq('Hello, how are you?') + expect(configuration.temperature).to eq(0.7) + expect(configuration.provider).to eq('openai') + expect(configuration.model).to eq('gpt-3.5-turbo') + end + end end \ No newline at end of file From c8db6afeeceb28c1b8b14d2d6e1cb6b61b88840e Mon Sep 17 00:00:00 2001 From: Reion19 Date: Thu, 12 Jun 2025 20:17:37 +0300 Subject: [PATCH 4/4] Fix header assignment and update model list descriptions in specs --- lib/rubyai/client.rb | 2 +- lib/rubyai/configuration.rb | 2 -- spec/rubyai/providers/anthropic_spec.rb | 2 +- spec/rubyai/providers/openai_spec.rb | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/rubyai/client.rb b/lib/rubyai/client.rb index c9fe4b6..7380792 100644 --- a/lib/rubyai/client.rb +++ b/lib/rubyai/client.rb @@ -9,7 +9,7 @@ def initialize(config = {}) def call response = connection.post do |req| req.url Configuration::PROVIDERS[configuration.provider] || Configuration::BASE_URL - req.headers = (HTTP.build_headers(configuration.provider || RubyAI::Configuration::DEFAULT_PROVIDER, RubyAI.config )) + req.headers.merge!(HTTP.build_headers(configuration.provider || RubyAI::Configuration::DEFAULT_PROVIDER, RubyAI.config )) req.body = HTTP.build_body(configuration.messages, configuration.provider, configuration.model, configuration.temperature).to_json end diff --git a/lib/rubyai/configuration.rb b/lib/rubyai/configuration.rb index 9c0ad27..3e0b9fa 100644 --- a/lib/rubyai/configuration.rb +++ b/lib/rubyai/configuration.rb @@ -23,8 +23,6 @@ class Configuration :temperature, :provider, # :providers - :provider, - :openai_api_key, :anthropic_api_key def initialize(config = {}) diff --git a/spec/rubyai/providers/anthropic_spec.rb b/spec/rubyai/providers/anthropic_spec.rb index 0c37ef0..ccc6792 100644 --- a/spec/rubyai/providers/anthropic_spec.rb +++ b/spec/rubyai/providers/anthropic_spec.rb @@ -2,7 +2,7 @@ RSpec.describe RubyAI::Providers::Anthropic do describe '.models' do - it 'should return an list of models' do + it 'should return a list of models' do expect(described_class.models).to eq( "claude-2" => "claude-2", "claude-instant-100k" => "claude-instant-100k", "claude-1" => "claude-1", diff --git a/spec/rubyai/providers/openai_spec.rb b/spec/rubyai/providers/openai_spec.rb index 636d80a..37b1f7d 100644 --- a/spec/rubyai/providers/openai_spec.rb +++ b/spec/rubyai/providers/openai_spec.rb @@ -2,7 +2,7 @@ RSpec.describe RubyAI::Providers::OpenAI do describe '.models' do - it 'should return an list of models' do + it 'should return a list of models' do expect(described_class.models).to eq("gpt-3.5-turbo" => "gpt-3.5-turbo", "gpt-4" => "gpt-4", "gpt-4-32k" => "gpt-4-32k",