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) diff --git a/lib/rubyai.rb b/lib/rubyai.rb index 211a3bc..5a9d381 100644 --- a/lib/rubyai.rb +++ b/lib/rubyai.rb @@ -2,11 +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 + end + + def self.chat(config = {}) + Client.new(config).call + end + + def self.configure + 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..7380792 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.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 JSON.parse(response.body) diff --git a/lib/rubyai/configuration.rb b/lib/rubyai/configuration.rb index ed0bc1b..3e0b9fa 100644 --- a/lib/rubyai/configuration.rb +++ b/lib/rubyai/configuration.rb @@ -1,31 +1,42 @@ 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 + :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/configuration_spec.rb b/spec/configuration_spec.rb deleted file mode 100644 index e9298f1..0000000 --- a/spec/configuration_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'webmock/rspec' -require_relative '../lib/rubyai/client.rb' - -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' } - - before do - RubyAI.configure do |config| - config.api_key = api_key - config.messages = messages - end - end - - describe '#call' do - let(:response_body) { { 'choices' => [{ 'message' => { 'content' => '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 via configuration' do - configuration = { api_key: RubyAI.configuration.api_key, messages: RubyAI.configuration.messages } - client = described_class.new(configuration) - result = client.call - expect(result.dig('choices', 0, 'message', 'content')).to eq('This is a response from the model.') - end - end -end 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/client_spec.rb b/spec/rubyai/client_spec.rb similarity index 83% rename from spec/client_spec.rb rename to spec/rubyai/client_spec.rb index f74c472..dfe3f03 100644 --- a/spec/client_spec.rb +++ b/spec/rubyai/client_spec.rb @@ -1,12 +1,13 @@ 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' } 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 new file mode 100644 index 0000000..81e8fe1 --- /dev/null +++ b/spec/rubyai/configuration_spec.rb @@ -0,0 +1,67 @@ +require 'webmock/rspec' +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 + end + + describe '#call' do + let(:response_body) { { 'choices' => [{ 'message' => { 'content' => '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 via configuration' do + configuration = { api_key: RubyAI.configuration.api_key, messages: RubyAI.configuration.messages } + client = described_class.new(configuration) + result = client.call + 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..ccc6792 --- /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 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", + "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..37b1f7d --- /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 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", + "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 new file mode 100644 index 0000000..bdcd471 --- /dev/null +++ b/spec/rubyai/rubyai_spec.rb @@ -0,0 +1,52 @@ +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) + 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(: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.' } } + 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 + + 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