llm.rb 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +83 -22
- data/lib/llm/conversation.rb +14 -2
- data/lib/llm/core_ext/ostruct.rb +0 -0
- data/lib/llm/error.rb +0 -0
- data/lib/llm/file.rb +0 -0
- data/lib/llm/http_client.rb +0 -0
- data/lib/llm/lazy_conversation.rb +14 -2
- data/lib/llm/message.rb +1 -1
- data/lib/llm/message_queue.rb +0 -0
- data/lib/llm/model.rb +7 -0
- data/lib/llm/provider.rb +117 -98
- data/lib/llm/providers/anthropic/error_handler.rb +1 -1
- data/lib/llm/providers/anthropic/format.rb +0 -0
- data/lib/llm/providers/anthropic/response_parser.rb +0 -0
- data/lib/llm/providers/anthropic.rb +31 -15
- data/lib/llm/providers/gemini/error_handler.rb +0 -0
- data/lib/llm/providers/gemini/format.rb +0 -0
- data/lib/llm/providers/gemini/response_parser.rb +0 -0
- data/lib/llm/providers/gemini.rb +25 -14
- data/lib/llm/providers/ollama/error_handler.rb +0 -0
- data/lib/llm/providers/ollama/format.rb +0 -0
- data/lib/llm/providers/ollama/response_parser.rb +13 -0
- data/lib/llm/providers/ollama.rb +32 -8
- data/lib/llm/providers/openai/error_handler.rb +0 -0
- data/lib/llm/providers/openai/format.rb +0 -0
- data/lib/llm/providers/openai/response_parser.rb +5 -3
- data/lib/llm/providers/openai.rb +22 -12
- data/lib/llm/providers/voyageai/error_handler.rb +32 -0
- data/lib/llm/providers/voyageai/response_parser.rb +13 -0
- data/lib/llm/providers/voyageai.rb +44 -0
- data/lib/llm/response/completion.rb +0 -0
- data/lib/llm/response/embedding.rb +0 -0
- data/lib/llm/response.rb +0 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +18 -8
- data/llm.gemspec +6 -1
- data/share/llm/models/anthropic.yml +35 -0
- data/share/llm/models/gemini.yml +35 -0
- data/share/llm/models/ollama.yml +155 -0
- data/share/llm/models/openai.yml +46 -0
- data/spec/anthropic/completion_spec.rb +11 -27
- data/spec/anthropic/embedding_spec.rb +25 -0
- data/spec/gemini/completion_spec.rb +13 -29
- data/spec/gemini/embedding_spec.rb +4 -12
- data/spec/llm/lazy_conversation_spec.rb +45 -63
- data/spec/ollama/completion_spec.rb +7 -16
- data/spec/ollama/embedding_spec.rb +14 -5
- data/spec/openai/completion_spec.rb +19 -43
- data/spec/openai/embedding_spec.rb +4 -12
- data/spec/readme_spec.rb +9 -12
- data/spec/setup.rb +7 -16
- metadata +81 -2
@@ -3,29 +3,11 @@
|
|
3
3
|
require "setup"
|
4
4
|
|
5
5
|
RSpec.describe "LLM::Anthropic: completions" do
|
6
|
-
subject(:anthropic) { LLM.anthropic(
|
6
|
+
subject(:anthropic) { LLM.anthropic(token) }
|
7
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
.with(headers: {"Content-Type" => "application/json"})
|
11
|
-
.to_return(
|
12
|
-
status: 200,
|
13
|
-
body: fixture("anthropic/completions/ok_completion.json"),
|
14
|
-
headers: {"Content-Type" => "application/json"}
|
15
|
-
)
|
16
|
-
end
|
17
|
-
|
18
|
-
before(:each, :unauthorized) do
|
19
|
-
stub_request(:post, "https://api.anthropic.com/v1/messages")
|
20
|
-
.with(headers: {"Content-Type" => "application/json"})
|
21
|
-
.to_return(
|
22
|
-
status: 403,
|
23
|
-
body: fixture("anthropic/completions/unauthorized_completion.json"),
|
24
|
-
headers: {"Content-Type" => "application/json"}
|
25
|
-
)
|
26
|
-
end
|
27
|
-
|
28
|
-
context "when given a successful response", :success do
|
9
|
+
context "when given a successful response",
|
10
|
+
vcr: {cassette_name: "anthropic/completions/successful_response"} do
|
29
11
|
subject(:response) { anthropic.complete("Hello, world", :user) }
|
30
12
|
|
31
13
|
it "returns a completion" do
|
@@ -38,9 +20,9 @@ RSpec.describe "LLM::Anthropic: completions" do
|
|
38
20
|
|
39
21
|
it "includes token usage" do
|
40
22
|
expect(response).to have_attributes(
|
41
|
-
prompt_tokens:
|
42
|
-
completion_tokens:
|
43
|
-
total_tokens:
|
23
|
+
prompt_tokens: 10,
|
24
|
+
completion_tokens: 30,
|
25
|
+
total_tokens: 40
|
44
26
|
)
|
45
27
|
end
|
46
28
|
|
@@ -50,7 +32,7 @@ RSpec.describe "LLM::Anthropic: completions" do
|
|
50
32
|
it "has choices" do
|
51
33
|
expect(choice).to have_attributes(
|
52
34
|
role: "assistant",
|
53
|
-
content: "
|
35
|
+
content: "Hello! How can I assist you today? Feel free to ask me any questions or let me know if you need help with anything."
|
54
36
|
)
|
55
37
|
end
|
56
38
|
|
@@ -60,8 +42,10 @@ RSpec.describe "LLM::Anthropic: completions" do
|
|
60
42
|
end
|
61
43
|
end
|
62
44
|
|
63
|
-
context "when given an unauthorized response",
|
45
|
+
context "when given an unauthorized response",
|
46
|
+
vcr: {cassette_name: "anthropic/completions/unauthorized_response"} do
|
64
47
|
subject(:response) { anthropic.complete("Hello", :user) }
|
48
|
+
let(:token) { "BADTOKEN" }
|
65
49
|
|
66
50
|
it "raises an error" do
|
67
51
|
expect { response }.to raise_error(LLM::Error::Unauthorized)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "setup"
|
4
|
+
|
5
|
+
RSpec.describe "LLM::Anthropic: embeddings" do
|
6
|
+
let(:anthropic) { LLM.anthropic(token) }
|
7
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
8
|
+
|
9
|
+
context "when given a successful response",
|
10
|
+
vcr: {cassette_name: "anthropic/embeddings/successful_response"} do
|
11
|
+
subject(:response) { anthropic.embed("Hello, world", token:) }
|
12
|
+
|
13
|
+
it "returns an embedding" do
|
14
|
+
expect(response).to be_instance_of(LLM::Response::Embedding)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "returns a model" do
|
18
|
+
expect(response.model).to eq("voyage-2")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "has embeddings" do
|
22
|
+
expect(response.embeddings).to be_instance_of(Array)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -3,44 +3,26 @@
|
|
3
3
|
require "setup"
|
4
4
|
|
5
5
|
RSpec.describe "LLM::Gemini: completions" do
|
6
|
-
subject(:gemini) { LLM.gemini(
|
6
|
+
subject(:gemini) { LLM.gemini(token) }
|
7
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
.to_return(
|
12
|
-
status: 200,
|
13
|
-
body: fixture("gemini/completions/ok_completion.json"),
|
14
|
-
headers: {"Content-Type" => "application/json"}
|
15
|
-
)
|
16
|
-
end
|
17
|
-
|
18
|
-
before(:each, :unauthorized) do
|
19
|
-
stub_request(:post, "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=")
|
20
|
-
.with(headers: {"Content-Type" => "application/json"})
|
21
|
-
.to_return(
|
22
|
-
status: 400,
|
23
|
-
body: fixture("gemini/completions/unauthorized_completion.json"),
|
24
|
-
headers: {"Content-Type" => "application/json"}
|
25
|
-
)
|
26
|
-
end
|
27
|
-
|
28
|
-
context "when given a successful response", :success do
|
29
|
-
subject(:response) { gemini.complete(LLM::Message.new("user", "Hello!")) }
|
9
|
+
context "when given a successful response",
|
10
|
+
vcr: {cassette_name: "gemini/completions/successful_response"} do
|
11
|
+
subject(:response) { gemini.complete("Hello!", :user) }
|
30
12
|
|
31
13
|
it "returns a completion" do
|
32
14
|
expect(response).to be_a(LLM::Response::Completion)
|
33
15
|
end
|
34
16
|
|
35
17
|
it "returns a model" do
|
36
|
-
expect(response.model).to eq("gemini-1.5-flash
|
18
|
+
expect(response.model).to eq("gemini-1.5-flash")
|
37
19
|
end
|
38
20
|
|
39
21
|
it "includes token usage" do
|
40
22
|
expect(response).to have_attributes(
|
41
23
|
prompt_tokens: 2,
|
42
|
-
completion_tokens:
|
43
|
-
total_tokens:
|
24
|
+
completion_tokens: 11,
|
25
|
+
total_tokens: 13
|
44
26
|
)
|
45
27
|
end
|
46
28
|
|
@@ -52,7 +34,7 @@ RSpec.describe "LLM::Gemini: completions" do
|
|
52
34
|
choices: [
|
53
35
|
have_attributes(
|
54
36
|
role: "model",
|
55
|
-
content: "Hello! How can I help you today
|
37
|
+
content: "Hello there! How can I help you today?\n"
|
56
38
|
)
|
57
39
|
]
|
58
40
|
)
|
@@ -64,8 +46,10 @@ RSpec.describe "LLM::Gemini: completions" do
|
|
64
46
|
end
|
65
47
|
end
|
66
48
|
|
67
|
-
context "when given an unauthorized response",
|
68
|
-
|
49
|
+
context "when given an unauthorized response",
|
50
|
+
vcr: {cassette_name: "gemini/completions/unauthorized_response"} do
|
51
|
+
subject(:response) { gemini.complete("Hello!", :user) }
|
52
|
+
let(:token) { "BADTOKEN" }
|
69
53
|
|
70
54
|
it "raises an error" do
|
71
55
|
expect { response }.to raise_error(LLM::Error::Unauthorized)
|
@@ -3,19 +3,11 @@
|
|
3
3
|
require "setup"
|
4
4
|
|
5
5
|
RSpec.describe "LLM::OpenAI: embeddings" do
|
6
|
-
let(:gemini) { LLM.gemini(
|
6
|
+
let(:gemini) { LLM.gemini(token) }
|
7
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
.with(headers: {"Content-Type" => "application/json"})
|
11
|
-
.to_return(
|
12
|
-
status: 200,
|
13
|
-
body: fixture("gemini/embeddings/hello_world_embedding.json"),
|
14
|
-
headers: {"Content-Type" => "application/json"}
|
15
|
-
)
|
16
|
-
end
|
17
|
-
|
18
|
-
context "when given a successful response", :success do
|
9
|
+
context "when given a successful response",
|
10
|
+
vcr: {cassette_name: "gemini/embeddings/successful_response"} do
|
19
11
|
subject(:response) { gemini.embed("Hello, world") }
|
20
12
|
|
21
13
|
it "returns an embedding" do
|
@@ -3,106 +3,88 @@
|
|
3
3
|
require "setup"
|
4
4
|
|
5
5
|
RSpec.describe LLM::LazyConversation do
|
6
|
-
|
7
|
-
|
6
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
7
|
+
let(:prompt) { "Keep your answers short and concise, and provide three answers to the three questions" }
|
8
|
+
|
9
|
+
context "with gemini",
|
10
|
+
vcr: {cassette_name: "gemini/lazy_conversation/successful_response"} do
|
11
|
+
let(:provider) { LLM.gemini(token) }
|
8
12
|
let(:conversation) { described_class.new(provider) }
|
9
13
|
|
10
14
|
context "when given a thread of messages" do
|
11
15
|
subject(:message) { conversation.messages.to_a[-1] }
|
12
16
|
|
13
17
|
before do
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
)
|
19
|
-
.to_return(
|
20
|
-
status: 200,
|
21
|
-
body: response_fixture("gemini/completions/ok_completion.json"),
|
22
|
-
headers: {"Content-Type" => "application/json"}
|
23
|
-
)
|
24
|
-
end
|
25
|
-
|
26
|
-
before do
|
27
|
-
conversation.chat "Hello"
|
28
|
-
conversation.chat "I have a question"
|
29
|
-
conversation.chat "How are you?"
|
18
|
+
conversation.chat prompt
|
19
|
+
conversation.chat "What is 3+2 ?"
|
20
|
+
conversation.chat "What is 5+5 ?"
|
21
|
+
conversation.chat "What is 5+7 ?"
|
30
22
|
end
|
31
23
|
|
32
24
|
it "maintains a conversation" do
|
33
|
-
|
25
|
+
is_expected.to have_attributes(
|
34
26
|
role: "model",
|
35
|
-
content: "
|
27
|
+
content: "5\n10\n12\n"
|
36
28
|
)
|
37
29
|
end
|
38
30
|
end
|
39
31
|
end
|
40
32
|
|
41
|
-
context "with openai"
|
42
|
-
let(:provider) { LLM.openai(
|
33
|
+
context "with openai" do
|
34
|
+
let(:provider) { LLM.openai(token) }
|
43
35
|
let(:conversation) { described_class.new(provider) }
|
44
36
|
|
45
|
-
context "when given a thread of messages"
|
46
|
-
|
47
|
-
|
48
|
-
before do
|
49
|
-
stub_request(:post, "https://api.openai.com/v1/chat/completions")
|
50
|
-
.with(
|
51
|
-
headers: {"Content-Type" => "application/json"},
|
52
|
-
body: request_fixture("openai/completions/ok_completion.json")
|
53
|
-
)
|
54
|
-
.to_return(
|
55
|
-
status: 200,
|
56
|
-
body: response_fixture("openai/completions/ok_completion.json"),
|
57
|
-
headers: {"Content-Type" => "application/json"}
|
58
|
-
)
|
59
|
-
end
|
37
|
+
context "when given a thread of messages",
|
38
|
+
vcr: {cassette_name: "openai/lazy_conversation/successful_response"} do
|
39
|
+
subject(:message) { conversation.recent_message }
|
60
40
|
|
61
41
|
before do
|
62
|
-
conversation.chat
|
63
|
-
conversation.chat "
|
64
|
-
conversation.chat "
|
42
|
+
conversation.chat prompt, :system
|
43
|
+
conversation.chat "What is 3+2 ?"
|
44
|
+
conversation.chat "What is 5+5 ?"
|
45
|
+
conversation.chat "What is 5+7 ?"
|
65
46
|
end
|
66
47
|
|
67
48
|
it "maintains a conversation" do
|
68
|
-
|
49
|
+
is_expected.to have_attributes(
|
69
50
|
role: "assistant",
|
70
|
-
content: "
|
51
|
+
content: "1. 5 \n2. 10 \n3. 12 "
|
71
52
|
)
|
72
53
|
end
|
73
54
|
end
|
55
|
+
|
56
|
+
context "when given a specific model",
|
57
|
+
vcr: {cassette_name: "openai/lazy_conversation/successful_response_o3_mini"} do
|
58
|
+
let(:conversation) { described_class.new(provider, model: provider.models["o3-mini"]) }
|
59
|
+
|
60
|
+
it "maintains the model throughout a conversation" do
|
61
|
+
conversation.chat(prompt, :system)
|
62
|
+
expect(conversation.recent_message.extra[:completion].model).to eq("o3-mini-2025-01-31")
|
63
|
+
conversation.chat("What is 5+5?")
|
64
|
+
expect(conversation.recent_message.extra[:completion].model).to eq("o3-mini-2025-01-31")
|
65
|
+
end
|
66
|
+
end
|
74
67
|
end
|
75
68
|
|
76
|
-
context "with ollama"
|
77
|
-
|
69
|
+
context "with ollama",
|
70
|
+
vcr: {cassette_name: "ollama/lazy_conversation/successful_response"} do
|
71
|
+
let(:provider) { LLM.ollama(nil, host: "eel.home.network") }
|
78
72
|
let(:conversation) { described_class.new(provider) }
|
79
73
|
|
80
74
|
context "when given a thread of messages" do
|
81
|
-
subject(:message) { conversation.
|
82
|
-
|
83
|
-
before do
|
84
|
-
stub_request(:post, "http://localhost:11434/api/chat")
|
85
|
-
.with(
|
86
|
-
headers: {"Content-Type" => "application/json"},
|
87
|
-
body: request_fixture("ollama/completions/ok_completion.json")
|
88
|
-
)
|
89
|
-
.to_return(
|
90
|
-
status: 200,
|
91
|
-
body: response_fixture("ollama/completions/ok_completion.json"),
|
92
|
-
headers: {"Content-Type" => "application/json"}
|
93
|
-
)
|
94
|
-
end
|
75
|
+
subject(:message) { conversation.recent_message }
|
95
76
|
|
96
77
|
before do
|
97
|
-
conversation.chat
|
98
|
-
conversation.chat "
|
99
|
-
conversation.chat "
|
78
|
+
conversation.chat prompt, :system
|
79
|
+
conversation.chat "What is 3+2 ?"
|
80
|
+
conversation.chat "What is 5+5 ?"
|
81
|
+
conversation.chat "What is 5+7 ?"
|
100
82
|
end
|
101
83
|
|
102
84
|
it "maintains a conversation" do
|
103
|
-
|
85
|
+
is_expected.to have_attributes(
|
104
86
|
role: "assistant",
|
105
|
-
content: "
|
87
|
+
content: "Here are the calculations:\n\n1. 3 + 2 = 5\n2. 5 + 5 = 10\n3. 5 + 7 = 12"
|
106
88
|
)
|
107
89
|
end
|
108
90
|
end
|
@@ -3,19 +3,10 @@
|
|
3
3
|
require "setup"
|
4
4
|
|
5
5
|
RSpec.describe "LLM::Ollama: completions" do
|
6
|
-
|
7
|
-
|
8
|
-
before(:each, :success) do
|
9
|
-
stub_request(:post, "localhost:11434/api/chat")
|
10
|
-
.with(headers: {"Content-Type" => "application/json"})
|
11
|
-
.to_return(
|
12
|
-
status: 200,
|
13
|
-
body: fixture("ollama/completions/ok_completion.json"),
|
14
|
-
headers: {"Content-Type" => "application/json"}
|
15
|
-
)
|
16
|
-
end
|
6
|
+
let(:ollama) { LLM.ollama(nil, host: "eel.home.network") }
|
17
7
|
|
18
|
-
context "when given a successful response",
|
8
|
+
context "when given a successful response",
|
9
|
+
vcr: {cassette_name: "ollama/completions/successful_response"} do
|
19
10
|
subject(:response) { ollama.complete("Hello!", :user) }
|
20
11
|
|
21
12
|
it "returns a completion" do
|
@@ -28,9 +19,9 @@ RSpec.describe "LLM::Ollama: completions" do
|
|
28
19
|
|
29
20
|
it "includes token usage" do
|
30
21
|
expect(response).to have_attributes(
|
31
|
-
prompt_tokens:
|
32
|
-
completion_tokens:
|
33
|
-
total_tokens:
|
22
|
+
prompt_tokens: 27,
|
23
|
+
completion_tokens: 26,
|
24
|
+
total_tokens: 53
|
34
25
|
)
|
35
26
|
end
|
36
27
|
|
@@ -40,7 +31,7 @@ RSpec.describe "LLM::Ollama: completions" do
|
|
40
31
|
it "has choices" do
|
41
32
|
expect(choice).to have_attributes(
|
42
33
|
role: "assistant",
|
43
|
-
content: "Hello!
|
34
|
+
content: "Hello! It's nice to meet you. Is there something I can help you with, or would you like to chat?"
|
44
35
|
)
|
45
36
|
end
|
46
37
|
|
@@ -3,13 +3,22 @@
|
|
3
3
|
require "setup"
|
4
4
|
|
5
5
|
RSpec.describe "LLM::Ollama: embeddings" do
|
6
|
-
let(:ollama) { LLM.ollama("") }
|
6
|
+
let(:ollama) { LLM.ollama(nil, host: "eel.home.network") }
|
7
7
|
|
8
|
-
context "when given a successful response",
|
9
|
-
|
8
|
+
context "when given a successful response",
|
9
|
+
vcr: {cassette_name: "ollama/embeddings/successful_response"} do
|
10
|
+
subject(:response) { ollama.embed(["This is a paragraph", "This is another one"]) }
|
10
11
|
|
11
|
-
it "
|
12
|
-
expect
|
12
|
+
it "returns an embedding" do
|
13
|
+
expect(response).to be_instance_of(LLM::Response::Embedding)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "returns a model" do
|
17
|
+
expect(response.model).to eq("llama3.2")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "has embeddings" do
|
21
|
+
expect(response.embeddings.size).to eq(2)
|
13
22
|
end
|
14
23
|
end
|
15
24
|
end
|
@@ -3,38 +3,11 @@
|
|
3
3
|
require "setup"
|
4
4
|
|
5
5
|
RSpec.describe "LLM::OpenAI: completions" do
|
6
|
-
subject(:openai) { LLM.openai(
|
6
|
+
subject(:openai) { LLM.openai(token) }
|
7
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
.with(headers: {"Content-Type" => "application/json"})
|
11
|
-
.to_return(
|
12
|
-
status: 200,
|
13
|
-
body: fixture("openai/completions/ok_completion.json"),
|
14
|
-
headers: {"Content-Type" => "application/json"}
|
15
|
-
)
|
16
|
-
end
|
17
|
-
|
18
|
-
before(:each, :unauthorized) do
|
19
|
-
stub_request(:post, "https://api.openai.com/v1/chat/completions")
|
20
|
-
.with(headers: {"Content-Type" => "application/json"})
|
21
|
-
.to_return(
|
22
|
-
status: 401,
|
23
|
-
body: fixture("openai/completions/unauthorized_completion.json"),
|
24
|
-
headers: {"Content-Type" => "application/json"}
|
25
|
-
)
|
26
|
-
end
|
27
|
-
|
28
|
-
before(:each, :bad_request) do
|
29
|
-
stub_request(:post, "https://api.openai.com/v1/chat/completions")
|
30
|
-
.with(headers: {"Content-Type" => "application/json"})
|
31
|
-
.to_return(
|
32
|
-
status: 400,
|
33
|
-
body: fixture("openai/completions/badrequest_completion.json")
|
34
|
-
)
|
35
|
-
end
|
36
|
-
|
37
|
-
context "when given a successful response", :success do
|
9
|
+
context "when given a successful response",
|
10
|
+
vcr: {cassette_name: "openai/completions/successful_response"} do
|
38
11
|
subject(:response) { openai.complete("Hello!", :user) }
|
39
12
|
|
40
13
|
it "returns a completion" do
|
@@ -48,8 +21,8 @@ RSpec.describe "LLM::OpenAI: completions" do
|
|
48
21
|
it "includes token usage" do
|
49
22
|
expect(response).to have_attributes(
|
50
23
|
prompt_tokens: 9,
|
51
|
-
completion_tokens:
|
52
|
-
total_tokens:
|
24
|
+
completion_tokens: 10,
|
25
|
+
total_tokens: 19
|
53
26
|
)
|
54
27
|
end
|
55
28
|
|
@@ -69,31 +42,34 @@ RSpec.describe "LLM::OpenAI: completions" do
|
|
69
42
|
end
|
70
43
|
end
|
71
44
|
|
72
|
-
context "when given
|
73
|
-
|
45
|
+
context "when given a 'bad request' response",
|
46
|
+
vcr: {cassette_name: "openai/completions/bad_request"} do
|
47
|
+
subject(:response) { openai.complete(URI("/foobar.exe"), :user) }
|
74
48
|
|
75
49
|
it "raises an error" do
|
76
|
-
expect { response }.to raise_error(LLM::Error::
|
50
|
+
expect { response }.to raise_error(LLM::Error::BadResponse)
|
77
51
|
end
|
78
52
|
|
79
53
|
it "includes the response" do
|
80
54
|
response
|
81
|
-
rescue LLM::Error
|
82
|
-
expect(ex.response).to
|
55
|
+
rescue LLM::Error => ex
|
56
|
+
expect(ex.response).to be_instance_of(Net::HTTPBadRequest)
|
83
57
|
end
|
84
58
|
end
|
85
59
|
|
86
|
-
context "when given
|
87
|
-
|
60
|
+
context "when given an unauthorized response",
|
61
|
+
vcr: {cassette_name: "openai/completions/unauthorized_response"} do
|
62
|
+
subject(:response) { openai.complete(LLM::Message.new("Hello!", :user)) }
|
63
|
+
let(:token) { "BADTOKEN" }
|
88
64
|
|
89
65
|
it "raises an error" do
|
90
|
-
expect { response }.to raise_error(LLM::Error::
|
66
|
+
expect { response }.to raise_error(LLM::Error::Unauthorized)
|
91
67
|
end
|
92
68
|
|
93
69
|
it "includes the response" do
|
94
70
|
response
|
95
|
-
rescue LLM::Error => ex
|
96
|
-
expect(ex.response).to
|
71
|
+
rescue LLM::Error::Unauthorized => ex
|
72
|
+
expect(ex.response).to be_kind_of(Net::HTTPResponse)
|
97
73
|
end
|
98
74
|
end
|
99
75
|
end
|
@@ -3,19 +3,11 @@
|
|
3
3
|
require "setup"
|
4
4
|
|
5
5
|
RSpec.describe "LLM::OpenAI: embeddings" do
|
6
|
-
let(:openai) { LLM.openai(
|
6
|
+
let(:openai) { LLM.openai(token) }
|
7
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
.with(headers: {"Content-Type" => "application/json"})
|
11
|
-
.to_return(
|
12
|
-
status: 200,
|
13
|
-
body: fixture("openai/embeddings/hello_world_embedding.json"),
|
14
|
-
headers: {"Content-Type" => "application/json"}
|
15
|
-
)
|
16
|
-
end
|
17
|
-
|
18
|
-
context "when given a successful response", :success do
|
9
|
+
context "when given a successful response",
|
10
|
+
vcr: {cassette_name: "openai/embeddings/successful_response"} do
|
19
11
|
subject(:response) { openai.embed("Hello, world") }
|
20
12
|
|
21
13
|
it "returns an embedding" do
|
data/spec/readme_spec.rb
CHANGED
@@ -22,22 +22,19 @@ RSpec.describe "The README examples" do
|
|
22
22
|
|
23
23
|
let(:expected_conversation) do
|
24
24
|
[
|
25
|
-
"[system] You are
|
26
|
-
"
|
25
|
+
"[system] You are my math assistant.",
|
26
|
+
"I will provide you with (simple) equations.",
|
27
|
+
"You will provide answers in the format \"The answer to <equation> is <answer>\".",
|
27
28
|
"I will provide you a set of messages. Reply to all of them.",
|
28
29
|
"A message is considered unanswered if there is no corresponding assistant response.",
|
29
30
|
|
30
|
-
"[user]
|
31
|
-
"[user]
|
32
|
-
"[user]
|
31
|
+
"[user] Tell me the answer to 5 + 15",
|
32
|
+
"[user] Tell me the answer to (5 + 15) * 2",
|
33
|
+
"[user] Tell me the answer to ((5 + 15) * 2) / 10",
|
33
34
|
|
34
|
-
"[assistant] The
|
35
|
-
"
|
36
|
-
"
|
37
|
-
"I love Ruby too! Did you know that a Ruby is not only a beautiful",
|
38
|
-
"gemstone, but it's also a programming language that's both elegant",
|
39
|
-
"and powerful! Speaking of colors, why did the orange stop?",
|
40
|
-
"Because it ran out of juice!"
|
35
|
+
"[assistant] The answer to 5 + 15 is 20.",
|
36
|
+
"The answer to (5 + 15) * 2 is 40.",
|
37
|
+
"The answer to ((5 + 15) * 2) / 10 is 4."
|
41
38
|
].map(&:strip)
|
42
39
|
end
|
43
40
|
|
data/spec/setup.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "llm"
|
4
4
|
require "webmock/rspec"
|
5
|
+
require "vcr"
|
5
6
|
|
6
7
|
RSpec.configure do |config|
|
7
8
|
config.disable_monkey_patching!
|
@@ -9,21 +10,11 @@ RSpec.configure do |config|
|
|
9
10
|
config.expect_with :rspec do |c|
|
10
11
|
c.syntax = :expect
|
11
12
|
end
|
13
|
+
end
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
def response_fixture(file)
|
20
|
-
path = File.join(fixtures, "responses", file)
|
21
|
-
File.read(path).chomp
|
22
|
-
end
|
23
|
-
alias_method :fixture, :response_fixture
|
24
|
-
|
25
|
-
def fixtures
|
26
|
-
File.join(__dir__, "fixtures")
|
27
|
-
end
|
28
|
-
}
|
15
|
+
VCR.configure do |config|
|
16
|
+
config.cassette_library_dir = "spec/fixtures/cassettes"
|
17
|
+
config.hook_into :webmock
|
18
|
+
config.configure_rspec_metadata!
|
19
|
+
config.filter_sensitive_data("TOKEN") { ENV["LLM_SECRET"] }
|
29
20
|
end
|