Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 1 addition & 25 deletions lib/rubyai/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,7 @@ def initialize(provider,
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)

response = connection.post do |req|
req.url Provider::PROVIDERS[@provider, @model]
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}"
HTTP.connect(messages: messages, provider: provider, model: model, temperature: temperature)
end
end
end
26 changes: 1 addition & 25 deletions lib/rubyai/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,7 @@ def call
temperature = configuration.temperature
raise ArgumentError, "Messages cannot be empty" if messages.nil? || messages.empty?

body = HTTP.build_body(messages, provider, model, temperature)
headers = HTTP.build_headers(provider)

response = connection.post do |req|
req.url Provider::PROVIDERS[provider, model]
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}"
HTTP.connect(messages: messages, provider: provider, model: model, temperature: temperature)
end
end
end
33 changes: 33 additions & 0 deletions lib/rubyai/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,38 @@ def build_body(messages, provider, model, temperature)
def build_headers(provider)
RubyAI.config.send(provider).build_http_headers(provider)
end

def connect(messages:, provider: , model:, temperature:)
body = self.build_body(messages, provider, model, temperature)
headers = self.build_headers(provider)

response = case provider
when "bedrock_anthropic"
RubyAI.config.bedrock_anthropic.client.invoke_model(model_id: model,
body: BedrockAnthropic.build_http_body.to_json,
content_type: "application/json")
else
connection.post do |req|
req.url Provider::PROVIDERS[provider, model]
req.headers.merge!(headers)
req.body = body.to_json
end
end

JSON.parse(response.body)
end

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
92 changes: 32 additions & 60 deletions spec/rubyai/chat_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
require_relative "../../lib/rubyai"
require "webmock/rspec"

# spec/ruby_ai/chat_spec.rb

require "spec_helper"

RSpec.describe RubyAI::Chat do
Expand All @@ -22,11 +20,15 @@
end

it "uses default provider if none is given" do
allow(RubyAI).to receive_message_chain(:config,
:default_provider).and_return("default_provider")
allow(RubyAI).to receive_message_chain(:config, :default_provider).and_return("default_provider")
chat_instance = described_class.new(nil)
expect(chat_instance.provider).to eq("default_provider")
end

it "uses default temperature when not specified" do
chat_instance = described_class.new(provider, model: model)
expect(chat_instance.temperature).to eq(0.75)
end
end

describe "#call" do
Expand All @@ -38,81 +40,51 @@
end

context "when messages are valid" do
let(:fake_connection) { instance_double(Faraday::Connection) }
let(:fake_response) { instance_double(Faraday::Response, body: response_body) }
let(:fake_connection) do
instance_double(Faraday::Connection).tap do |conn|
allow(conn).to receive(:post).and_return(fake_response)
allow(conn).to receive(:headers).and_return({})
allow(conn).to receive(:adapter)
end
end

let(:url) { "https://fake.provider/api" }

before do
allow(RubyAI::HTTP).to receive(:build_body).with(messages, provider, model,
temperature).and_return({ body: "data" })
allow(RubyAI::HTTP).to receive(:build_headers).with(provider).and_return({ "Authorization" => "Bearer token" })

stub_const("RubyAI::Configuration::PROVIDERS", { [provider, model] => url })

allow(Faraday).to receive(:new).and_return(fake_connection)
allow(fake_connection).to receive(:headers).and_return({})
allow(fake_connection).to receive(:adapter)

allow(fake_connection).to receive(:post).and_return(fake_response)
end

it "returns parsed JSON response" do
result = chat.call(messages)
expect(result).to eq({ "reply" => "Hi!" })
allow(RubyAI::HTTP).to receive(:connect).with(
messages: messages,
provider: provider,
model: model,
temperature: temperature
).and_return(fake_response)
end
end

context "when Faraday connection fails" do
before do
allow(Faraday).to receive(:new).and_raise(Faraday::ConnectionFailed.new("no internet"))
it "calls HTTP.connect with correct parameters" do
chat.call(messages)
expect(RubyAI::HTTP).to have_received(:connect).with(
messages: messages,
provider: provider,
model: model,
temperature: temperature
)
end

it "raises a connection error" do
expect { chat.call(messages) }.to raise_error("Connection error: no internet")
it "returns the HTTP response" do
result = chat.call(messages)
expect(result).to eq(fake_response)
end
end

context "when JSON parsing fails" do
let(:bad_response) { instance_double(Faraday::Response, body: "not_json") }

context "when HTTP.connect raises Faraday::ConnectionFailed" do
before do
stub_const("RubyAI::Configuration::PROVIDERS", { [provider, model] => "fake_url" })

allow(RubyAI::HTTP).to receive(:build_body).and_return({})
allow(RubyAI::HTTP).to receive(:build_headers).and_return({})

allow(Faraday).to receive(:new).and_return(double(
headers: {},
adapter: nil,
post: bad_response
))

allow(JSON).to receive(:parse).and_raise(JSON::ParserError.new("unexpected token"))
allow(RubyAI::HTTP).to receive(:connect).and_raise(Faraday::ConnectionFailed.new("no internet"))
end

it "raises a JSON parse error" do
expect { chat.call(messages) }.to raise_error(JSON::ParserError, "unexpected token")
it "allows the connection error to propagate" do
expect { chat.call(messages) }.to raise_error(Faraday::ConnectionFailed, "no internet")
end
end

context "when any other error occurs" do
context "when HTTP.connect raises any other error" do
before do
allow(Faraday).to receive(:new).and_raise(StandardError.new("something went wrong"))
allow(RubyAI::HTTP).to receive(:connect).and_raise(StandardError.new("something went wrong"))
end

it "raises a generic error" do
expect do
chat.call(messages)
end.to raise_error("An unexpected error occurred: something went wrong")
it "allows the error to propagate" do
expect { chat.call(messages) }.to raise_error(StandardError, "something went wrong")
end
end
end
end
end
86 changes: 86 additions & 0 deletions spec/rubyai/client_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
require_relative "../../lib/rubyai"
require "webmock/rspec"
require "spec_helper"

RSpec.describe RubyAI::Client do
let(:config) { { provider: "openai", model: "gpt-4", temperature: 0.8, messages: ["Hello"] } }
let(:mock_configuration) { instance_double(RubyAI::Configuration) }
let(:response_body) { { "reply" => "Hi!" }.to_json }

subject(:client) { described_class.new(config) }

describe "#initialize" do
it "creates configuration from provided config" do
expect(RubyAI).to receive(:config).with(config).and_return(mock_configuration)
client_instance = described_class.new(config)
expect(client_instance.configuration).to eq(mock_configuration)
end

it "uses empty hash as default config" do
expect(RubyAI).to receive(:config).with({}).and_return(mock_configuration)
client_instance = described_class.new
expect(client_instance.configuration).to eq(mock_configuration)
end
end

describe "#call" do
let(:messages) { ["Hello", "How are you?"] }
let(:provider) { "openai" }
let(:model) { "gpt-4" }
let(:temperature) { 0.7 }
let(:fake_response) { instance_double(Faraday::Response, body: response_body) }

before do
allow(mock_configuration).to receive(:messages).and_return(messages)
allow(mock_configuration).to receive(:provider).and_return(provider)
allow(mock_configuration).to receive(:model).and_return(model)
allow(mock_configuration).to receive(:temperature).and_return(temperature)
allow(RubyAI).to receive(:config).and_return(mock_configuration)
end

context "when configuration is valid" do
before do
allow(RubyAI::HTTP).to receive(:connect).and_return(fake_response)
end

it "calls HTTP.connect with correct parameters" do
client.call
expect(RubyAI::HTTP).to have_received(:connect).with(
messages: messages,
provider: provider,
model: model,
temperature: temperature
)
end

it "returns the HTTP response" do
result = client.call
expect(result).to eq(fake_response)
end
end

context "when messages are nil or empty" do
it "raises ArgumentError for nil messages" do
allow(mock_configuration).to receive(:messages).and_return(nil)
expect { client.call }.to raise_error(ArgumentError, "Messages cannot be empty")
end

it "raises ArgumentError for empty messages" do
allow(mock_configuration).to receive(:messages).and_return([])
expect { client.call }.to raise_error(ArgumentError, "Messages cannot be empty")
end
end

context "when HTTP operations fail" do
it "propagates connection errors" do
allow(RubyAI::HTTP).to receive(:connect).and_raise(Faraday::ConnectionFailed.new("network error"))
expect { client.call }.to raise_error(Faraday::ConnectionFailed, "network error")
end

it "propagates any other errors" do
allow(RubyAI::HTTP).to receive(:connect).and_raise(StandardError.new("something went wrong"))
expect { client.call }.to raise_error(StandardError, "something went wrong")
end
end
end
end
Loading