llm.rb 0.2.1 → 0.3.1
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 +318 -110
- data/lib/llm/buffer.rb +83 -0
- data/lib/llm/chat.rb +131 -0
- data/lib/llm/error.rb +3 -3
- data/lib/llm/file.rb +36 -40
- data/lib/llm/message.rb +21 -8
- data/lib/llm/mime.rb +54 -0
- data/lib/llm/multipart.rb +100 -0
- data/lib/llm/provider.rb +123 -21
- data/lib/llm/providers/anthropic/error_handler.rb +3 -1
- data/lib/llm/providers/anthropic/format.rb +2 -0
- data/lib/llm/providers/anthropic/response_parser.rb +3 -1
- data/lib/llm/providers/anthropic.rb +14 -5
- data/lib/llm/providers/gemini/audio.rb +77 -0
- data/lib/llm/providers/gemini/error_handler.rb +4 -2
- data/lib/llm/providers/gemini/files.rb +162 -0
- data/lib/llm/providers/gemini/format.rb +12 -6
- data/lib/llm/providers/gemini/images.rb +99 -0
- data/lib/llm/providers/gemini/response_parser.rb +27 -1
- data/lib/llm/providers/gemini.rb +62 -6
- data/lib/llm/providers/ollama/error_handler.rb +3 -1
- data/lib/llm/providers/ollama/format.rb +13 -5
- data/lib/llm/providers/ollama/response_parser.rb +3 -1
- data/lib/llm/providers/ollama.rb +30 -7
- data/lib/llm/providers/openai/audio.rb +97 -0
- data/lib/llm/providers/openai/error_handler.rb +3 -1
- data/lib/llm/providers/openai/files.rb +148 -0
- data/lib/llm/providers/openai/format.rb +22 -8
- data/lib/llm/providers/openai/images.rb +109 -0
- data/lib/llm/providers/openai/response_parser.rb +58 -5
- data/lib/llm/providers/openai/responses.rb +85 -0
- data/lib/llm/providers/openai.rb +52 -6
- data/lib/llm/providers/voyageai/error_handler.rb +1 -1
- data/lib/llm/providers/voyageai.rb +2 -2
- data/lib/llm/response/audio.rb +13 -0
- data/lib/llm/response/audio_transcription.rb +14 -0
- data/lib/llm/response/audio_translation.rb +14 -0
- data/lib/llm/response/download_file.rb +15 -0
- data/lib/llm/response/file.rb +42 -0
- data/lib/llm/response/filelist.rb +18 -0
- data/lib/llm/response/image.rb +29 -0
- data/lib/llm/response/output.rb +56 -0
- data/lib/llm/response.rb +18 -6
- data/lib/llm/utils.rb +19 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +5 -2
- data/llm.gemspec +1 -6
- data/spec/anthropic/completion_spec.rb +1 -1
- data/spec/gemini/completion_spec.rb +1 -1
- data/spec/gemini/conversation_spec.rb +31 -0
- data/spec/gemini/files_spec.rb +124 -0
- data/spec/gemini/images_spec.rb +47 -0
- data/spec/llm/conversation_spec.rb +107 -62
- data/spec/ollama/completion_spec.rb +1 -1
- data/spec/ollama/conversation_spec.rb +31 -0
- data/spec/openai/audio_spec.rb +55 -0
- data/spec/openai/completion_spec.rb +5 -4
- data/spec/openai/files_spec.rb +204 -0
- data/spec/openai/images_spec.rb +95 -0
- data/spec/openai/responses_spec.rb +51 -0
- data/spec/setup.rb +8 -0
- metadata +31 -50
- data/LICENSE.txt +0 -21
- data/lib/llm/conversation.rb +0 -90
- data/lib/llm/http_client.rb +0 -29
- data/lib/llm/message_queue.rb +0 -54
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require "setup"
|
4
4
|
|
5
|
-
RSpec.describe "LLM::
|
5
|
+
RSpec.describe "LLM::Chat: non-lazy" do
|
6
6
|
shared_examples "a multi-turn conversation" do
|
7
7
|
context "when given a thread of messages" do
|
8
8
|
let(:inputs) do
|
@@ -57,91 +57,136 @@ RSpec.describe "LLM::Conversation: non-lazy" do
|
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
-
RSpec.describe "LLM::
|
61
|
-
let(:described_class) { LLM::
|
60
|
+
RSpec.describe "LLM::Chat: lazy" do
|
61
|
+
let(:described_class) { LLM::Chat }
|
62
62
|
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
63
|
-
let(:prompt)
|
63
|
+
let(:prompt) do
|
64
|
+
"Keep your answers short and concise, and provide three answers to the three questions" \
|
65
|
+
"There should be one answer per line" \
|
66
|
+
"An answer should be a number, for example: 5" \
|
67
|
+
"Nothing else"
|
68
|
+
end
|
64
69
|
|
65
|
-
context "
|
66
|
-
|
67
|
-
|
68
|
-
|
70
|
+
context "when given completions" do
|
71
|
+
context "with gemini",
|
72
|
+
vcr: {cassette_name: "gemini/lazy_conversation/successful_response"} do
|
73
|
+
let(:provider) { LLM.gemini(token) }
|
74
|
+
let(:conversation) { described_class.new(provider).lazy }
|
69
75
|
|
70
|
-
|
71
|
-
|
76
|
+
context "when given a thread of messages" do
|
77
|
+
subject(:message) { conversation.messages.to_a[-1] }
|
72
78
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
+
before do
|
80
|
+
conversation.chat prompt
|
81
|
+
conversation.chat "What is 3+2 ?"
|
82
|
+
conversation.chat "What is 5+5 ?"
|
83
|
+
conversation.chat "What is 5+7 ?"
|
84
|
+
end
|
79
85
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
86
|
+
it "maintains a conversation" do
|
87
|
+
is_expected.to have_attributes(
|
88
|
+
role: "model",
|
89
|
+
content: "5\n10\n12\n"
|
90
|
+
)
|
91
|
+
end
|
85
92
|
end
|
86
93
|
end
|
87
|
-
end
|
88
94
|
|
89
|
-
|
90
|
-
|
91
|
-
|
95
|
+
context "with openai" do
|
96
|
+
let(:provider) { LLM.openai(token) }
|
97
|
+
let(:conversation) { described_class.new(provider).lazy }
|
98
|
+
|
99
|
+
context "when given a thread of messages",
|
100
|
+
vcr: {cassette_name: "openai/lazy_conversation/completions/successful_response"} do
|
101
|
+
subject(:message) { conversation.recent_message }
|
92
102
|
|
93
|
-
|
94
|
-
|
95
|
-
|
103
|
+
before do
|
104
|
+
conversation.chat prompt, :system
|
105
|
+
conversation.chat "What is 3+2 ?"
|
106
|
+
conversation.chat "What is 5+5 ?"
|
107
|
+
conversation.chat "What is 5+7 ?"
|
108
|
+
end
|
96
109
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
110
|
+
it "maintains a conversation" do
|
111
|
+
is_expected.to have_attributes(
|
112
|
+
role: "assistant",
|
113
|
+
content: %r|5\s*\n10\s*\n12\s*|
|
114
|
+
)
|
115
|
+
end
|
102
116
|
end
|
103
117
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
118
|
+
context "when given a specific model",
|
119
|
+
vcr: {cassette_name: "openai/lazy_conversation/completions/successful_response_o3_mini"} do
|
120
|
+
let(:conversation) { described_class.new(provider, model: provider.models["o3-mini"]).lazy }
|
121
|
+
|
122
|
+
it "maintains the model throughout a conversation" do
|
123
|
+
conversation.chat(prompt, :system)
|
124
|
+
expect(conversation.recent_message.extra[:response].model).to eq("o3-mini-2025-01-31")
|
125
|
+
conversation.chat("What is 5+5?")
|
126
|
+
expect(conversation.recent_message.extra[:response].model).to eq("o3-mini-2025-01-31")
|
127
|
+
end
|
109
128
|
end
|
110
129
|
end
|
111
130
|
|
112
|
-
context "
|
113
|
-
vcr: {cassette_name: "
|
114
|
-
let(:
|
131
|
+
context "with ollama",
|
132
|
+
vcr: {cassette_name: "ollama/lazy_conversation/successful_response"} do
|
133
|
+
let(:provider) { LLM.ollama(nil, host: "eel.home.network") }
|
134
|
+
let(:conversation) { described_class.new(provider).lazy }
|
135
|
+
|
136
|
+
context "when given a thread of messages" do
|
137
|
+
subject(:message) { conversation.recent_message }
|
138
|
+
|
139
|
+
before do
|
140
|
+
conversation.chat prompt, :system
|
141
|
+
conversation.chat "What is 3+2 ?"
|
142
|
+
conversation.chat "What is 5+5 ?"
|
143
|
+
conversation.chat "What is 5+7 ?"
|
144
|
+
end
|
115
145
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
146
|
+
it "maintains a conversation" do
|
147
|
+
is_expected.to have_attributes(
|
148
|
+
role: "assistant",
|
149
|
+
content: "Here are the calculations:\n\n1. 3 + 2 = 5\n2. 5 + 5 = 10\n3. 5 + 7 = 12"
|
150
|
+
)
|
151
|
+
end
|
121
152
|
end
|
122
153
|
end
|
123
154
|
end
|
124
155
|
|
125
|
-
context "
|
126
|
-
|
127
|
-
|
128
|
-
|
156
|
+
context "when given responses" do
|
157
|
+
context "with openai" do
|
158
|
+
let(:provider) { LLM.openai(token) }
|
159
|
+
let(:conversation) { described_class.new(provider).lazy }
|
129
160
|
|
130
|
-
|
131
|
-
|
161
|
+
context "when given a thread of messages",
|
162
|
+
vcr: {cassette_name: "openai/lazy_conversation/responses/successful_response"} do
|
163
|
+
subject(:message) { conversation.recent_message }
|
132
164
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
165
|
+
before do
|
166
|
+
conversation.respond prompt, :developer
|
167
|
+
conversation.respond "What is 3+2 ?"
|
168
|
+
conversation.respond "What is 5+5 ?"
|
169
|
+
conversation.respond "What is 5+7 ?"
|
170
|
+
end
|
171
|
+
|
172
|
+
it "maintains a conversation" do
|
173
|
+
is_expected.to have_attributes(
|
174
|
+
role: "assistant",
|
175
|
+
content: %r|5\s*\n10\s*\n12\s*|
|
176
|
+
)
|
177
|
+
end
|
138
178
|
end
|
139
179
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
180
|
+
context "when given a specific model",
|
181
|
+
vcr: {cassette_name: "openai/lazy_conversation/responses/successful_response_o3_mini"} do
|
182
|
+
let(:conversation) { described_class.new(provider, model: provider.models["o3-mini"]).lazy }
|
183
|
+
|
184
|
+
it "maintains the model throughout a conversation" do
|
185
|
+
conversation.respond(prompt, :developer)
|
186
|
+
expect(conversation.last_message.extra[:response].model).to eq("o3-mini-2025-01-31")
|
187
|
+
conversation.respond("What is 5+5?")
|
188
|
+
expect(conversation.last_message.extra[:response].model).to eq("o3-mini-2025-01-31")
|
189
|
+
end
|
145
190
|
end
|
146
191
|
end
|
147
192
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "setup"
|
4
|
+
|
5
|
+
RSpec.describe "LLM::Chat: ollama" do
|
6
|
+
let(:described_class) { LLM::Chat }
|
7
|
+
let(:provider) { LLM.ollama(nil, host: "eel.home.network") }
|
8
|
+
let(:conversation) { described_class.new(provider, **params).lazy }
|
9
|
+
|
10
|
+
context "when asked to describe an image",
|
11
|
+
vcr: {cassette_name: "ollama/conversations/multimodal_response"} do
|
12
|
+
subject { conversation.last_message }
|
13
|
+
|
14
|
+
let(:params) { {model: "llava"} }
|
15
|
+
let(:image) { LLM::File("spec/fixtures/images/bluebook.png") }
|
16
|
+
|
17
|
+
before do
|
18
|
+
conversation.chat(image, :user)
|
19
|
+
conversation.chat("Describe the image with a short sentance", :user)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "describes the image" do
|
23
|
+
is_expected.to have_attributes(
|
24
|
+
role: "assistant",
|
25
|
+
content: " The image is a graphic illustration of a book" \
|
26
|
+
" with its pages spread out, symbolizing openness" \
|
27
|
+
" or knowledge. "
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "setup"
|
4
|
+
|
5
|
+
RSpec.describe "LLM::OpenAI::Audio" do
|
6
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
7
|
+
let(:provider) { LLM.openai(token) }
|
8
|
+
|
9
|
+
context "when given a successful create operation",
|
10
|
+
vcr: {cassette_name: "openai/audio/successful_create"} do
|
11
|
+
subject(:response) { provider.audio.create_speech(input: "A dog on a rocket to the moon") }
|
12
|
+
|
13
|
+
it "is successful" do
|
14
|
+
expect(response).to be_instance_of(LLM::Response::Audio)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "returns an audio" do
|
18
|
+
expect(response.audio).to be_instance_of(StringIO)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "when given a successful transcription operation",
|
23
|
+
vcr: {cassette_name: "openai/audio/successful_transcription"} do
|
24
|
+
subject(:response) do
|
25
|
+
provider.audio.create_transcription(
|
26
|
+
file: LLM::File("spec/fixtures/audio/rocket.mp3")
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "is successful" do
|
31
|
+
expect(response).to be_instance_of(LLM::Response::AudioTranscription)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "returns a transcription" do
|
35
|
+
expect(response.text).to eq("A dog on a rocket to the moon.")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "when given a successful translation operation",
|
40
|
+
vcr: {cassette_name: "openai/audio/successful_translation"} do
|
41
|
+
subject(:response) do
|
42
|
+
provider.audio.create_translation(
|
43
|
+
file: LLM::File("spec/fixtures/audio/bismillah.mp3")
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "is successful" do
|
48
|
+
expect(response).to be_instance_of(LLM::Response::AudioTranslation)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "returns a translation (Arabic => English)" do
|
52
|
+
expect(response.text).to eq("In the name of Allah, the Beneficent, the Merciful.")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -37,7 +37,7 @@ RSpec.describe "LLM::OpenAI: completions" do
|
|
37
37
|
end
|
38
38
|
|
39
39
|
it "includes the response" do
|
40
|
-
expect(choice.extra[:
|
40
|
+
expect(choice.extra[:response]).to eq(response)
|
41
41
|
end
|
42
42
|
end
|
43
43
|
end
|
@@ -47,7 +47,8 @@ RSpec.describe "LLM::OpenAI: completions" do
|
|
47
47
|
subject(:response) do
|
48
48
|
openai.complete "What is your name? What age are you?", :user, messages: [
|
49
49
|
{role: "system", content: "Answer all of my questions"},
|
50
|
-
{role: "system", content: "
|
50
|
+
{role: "system", content: "Answer in the format: My name is <name> and I am <age> years old"},
|
51
|
+
{role: "system", content: "Your name is Pablo and you are 25 years old"},
|
51
52
|
]
|
52
53
|
end
|
53
54
|
|
@@ -56,7 +57,7 @@ RSpec.describe "LLM::OpenAI: completions" do
|
|
56
57
|
choices: [
|
57
58
|
have_attributes(
|
58
59
|
role: "assistant",
|
59
|
-
content:
|
60
|
+
content: %r|\AMy name is Pablo and I am 25 years old|
|
60
61
|
)
|
61
62
|
]
|
62
63
|
)
|
@@ -68,7 +69,7 @@ RSpec.describe "LLM::OpenAI: completions" do
|
|
68
69
|
subject(:response) { openai.complete(URI("/foobar.exe"), :user) }
|
69
70
|
|
70
71
|
it "raises an error" do
|
71
|
-
expect { response }.to raise_error(LLM::Error::
|
72
|
+
expect { response }.to raise_error(LLM::Error::ResponseError)
|
72
73
|
end
|
73
74
|
|
74
75
|
it "includes the response" do
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "setup"
|
4
|
+
|
5
|
+
RSpec.describe "LLM::OpenAI::Files" do
|
6
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
7
|
+
let(:provider) { LLM.openai(token) }
|
8
|
+
|
9
|
+
context "when given a successful create operation (haiku1.txt)",
|
10
|
+
vcr: {cassette_name: "openai/files/successful_create_haiku1"} do
|
11
|
+
subject(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/haiku1.txt")) }
|
12
|
+
|
13
|
+
it "is successful" do
|
14
|
+
expect(file).to be_instance_of(LLM::Response::File)
|
15
|
+
ensure
|
16
|
+
provider.files.delete(file:)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "returns a file object" do
|
20
|
+
expect(file).to have_attributes(
|
21
|
+
id: instance_of(String),
|
22
|
+
filename: "haiku1.txt",
|
23
|
+
purpose: "assistants"
|
24
|
+
)
|
25
|
+
ensure
|
26
|
+
provider.files.delete(file:)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "when given a successful create operation (haiku2.txt)",
|
31
|
+
vcr: {cassette_name: "openai/files/successful_create_haiku2"} do
|
32
|
+
subject(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/haiku2.txt")) }
|
33
|
+
|
34
|
+
it "is successful" do
|
35
|
+
expect(file).to be_instance_of(LLM::Response::File)
|
36
|
+
ensure
|
37
|
+
provider.files.delete(file:)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns a file object" do
|
41
|
+
expect(file).to have_attributes(
|
42
|
+
id: instance_of(String),
|
43
|
+
filename: "haiku2.txt",
|
44
|
+
purpose: "assistants"
|
45
|
+
)
|
46
|
+
ensure
|
47
|
+
provider.files.delete(file:)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when given a successful delete operation (haiku3.txt)",
|
52
|
+
vcr: {cassette_name: "openai/files/successful_delete_haiku3"} do
|
53
|
+
let(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/haiku3.txt")) }
|
54
|
+
subject { provider.files.delete(file:) }
|
55
|
+
|
56
|
+
it "is successful" do
|
57
|
+
is_expected.to be_instance_of(OpenStruct)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "returns deleted status" do
|
61
|
+
is_expected.to have_attributes(
|
62
|
+
deleted: true
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "when given a successful get operation (haiku4.txt)",
|
68
|
+
vcr: {cassette_name: "openai/files/successful_get_haiku4"} do
|
69
|
+
let(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/haiku4.txt")) }
|
70
|
+
subject { provider.files.get(file:) }
|
71
|
+
|
72
|
+
it "is successful" do
|
73
|
+
is_expected.to be_instance_of(LLM::Response::File)
|
74
|
+
ensure
|
75
|
+
provider.files.delete(file:)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "returns a file object" do
|
79
|
+
is_expected.to have_attributes(
|
80
|
+
id: instance_of(String),
|
81
|
+
filename: "haiku4.txt",
|
82
|
+
purpose: "assistants"
|
83
|
+
)
|
84
|
+
ensure
|
85
|
+
provider.files.delete(file:)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "when given a successful all operation",
|
90
|
+
vcr: {cassette_name: "openai/files/successful_all"} do
|
91
|
+
let!(:files) do
|
92
|
+
[
|
93
|
+
provider.files.create(file: LLM::File("spec/fixtures/documents/haiku1.txt")),
|
94
|
+
provider.files.create(file: LLM::File("spec/fixtures/documents/haiku2.txt"))
|
95
|
+
]
|
96
|
+
end
|
97
|
+
subject(:file) { provider.files.all }
|
98
|
+
|
99
|
+
it "is successful" do
|
100
|
+
expect(file).to be_instance_of(LLM::Response::FileList)
|
101
|
+
ensure
|
102
|
+
files.each { |file| provider.files.delete(file:) }
|
103
|
+
end
|
104
|
+
|
105
|
+
it "returns an array of file objects" do
|
106
|
+
expect(file).to match_array(
|
107
|
+
[
|
108
|
+
have_attributes(
|
109
|
+
id: instance_of(String),
|
110
|
+
filename: "haiku1.txt",
|
111
|
+
purpose: "assistants"
|
112
|
+
),
|
113
|
+
have_attributes(
|
114
|
+
id: instance_of(String),
|
115
|
+
filename: "haiku2.txt",
|
116
|
+
purpose: "assistants"
|
117
|
+
)
|
118
|
+
]
|
119
|
+
)
|
120
|
+
ensure
|
121
|
+
files.each { |file| provider.files.delete(file:) }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context "when asked to describe the contents of a file",
|
126
|
+
vcr: {cassette_name: "openai/files/describe_freebsd.sysctl.pdf"} do
|
127
|
+
subject { bot.last_message.content.downcase[0..2] }
|
128
|
+
let(:bot) { LLM::Chat.new(provider).lazy }
|
129
|
+
let(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/freebsd.sysctl.pdf")) }
|
130
|
+
|
131
|
+
before do
|
132
|
+
bot.respond(file)
|
133
|
+
bot.respond("Is this PDF document about FreeBSD?")
|
134
|
+
bot.respond("Answer with yes or no. Nothing else.")
|
135
|
+
end
|
136
|
+
|
137
|
+
it "describes the document" do
|
138
|
+
is_expected.to eq("yes")
|
139
|
+
ensure
|
140
|
+
provider.files.delete(file:)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context "when asked to describe the contents of a file",
|
145
|
+
vcr: {cassette_name: "openai/files/describe_freebsd.sysctl_2.pdf"} do
|
146
|
+
subject { bot.last_message.content.downcase[0..2] }
|
147
|
+
let(:bot) { LLM::Chat.new(provider).lazy }
|
148
|
+
let(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/freebsd.sysctl.pdf")) }
|
149
|
+
|
150
|
+
before do
|
151
|
+
bot.respond([
|
152
|
+
"Is this PDF document about FreeBSD?",
|
153
|
+
"Answer with yes or no. Nothing else.",
|
154
|
+
file
|
155
|
+
])
|
156
|
+
end
|
157
|
+
|
158
|
+
it "describes the document" do
|
159
|
+
is_expected.to eq("yes")
|
160
|
+
ensure
|
161
|
+
provider.files.delete(file:)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context "when asked to describe the contents of a file",
|
166
|
+
vcr: {cassette_name: "openai/files/describe_freebsd.sysctl_3.pdf"} do
|
167
|
+
subject { bot.last_message.content.downcase[0..2] }
|
168
|
+
let(:bot) { LLM::Chat.new(provider).lazy }
|
169
|
+
let(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/freebsd.sysctl.pdf")) }
|
170
|
+
|
171
|
+
before do
|
172
|
+
bot.chat(file)
|
173
|
+
bot.chat("Is this PDF document about FreeBSD?")
|
174
|
+
bot.chat("Answer with yes or no. Nothing else.")
|
175
|
+
end
|
176
|
+
|
177
|
+
it "describes the document" do
|
178
|
+
is_expected.to eq("yes")
|
179
|
+
ensure
|
180
|
+
provider.files.delete(file:)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context "when asked to describe the contents of a file",
|
185
|
+
vcr: {cassette_name: "openai/files/describe_freebsd.sysctl_4.pdf"} do
|
186
|
+
subject { bot.last_message.content.downcase[0..2] }
|
187
|
+
let(:bot) { LLM::Chat.new(provider).lazy }
|
188
|
+
let(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/freebsd.sysctl.pdf")) }
|
189
|
+
|
190
|
+
before do
|
191
|
+
bot.chat([
|
192
|
+
"Is this PDF document about FreeBSD?",
|
193
|
+
"Answer with yes or no. Nothing else.",
|
194
|
+
file
|
195
|
+
])
|
196
|
+
end
|
197
|
+
|
198
|
+
it "describes the document" do
|
199
|
+
is_expected.to eq("yes")
|
200
|
+
ensure
|
201
|
+
provider.files.delete(file:)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "setup"
|
4
|
+
|
5
|
+
RSpec.describe "LLM::OpenAI::Images" do
|
6
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
7
|
+
let(:provider) { LLM.openai(token) }
|
8
|
+
|
9
|
+
context "when given a successful create operation (urls)",
|
10
|
+
vcr: {cassette_name: "openai/images/successful_create_urls"} do
|
11
|
+
subject(:response) { provider.images.create(prompt: "A dog on a rocket to the moon") }
|
12
|
+
|
13
|
+
it "is successful" do
|
14
|
+
expect(response).to be_instance_of(LLM::Response::Image)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "returns an array of urls" do
|
18
|
+
expect(response.urls).to be_instance_of(Array)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns a url" do
|
22
|
+
expect(response.urls[0]).to be_instance_of(String)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when given a successful create operation (base64)",
|
27
|
+
vcr: {cassette_name: "openai/images/successful_create_base64"} do
|
28
|
+
subject(:response) do
|
29
|
+
provider.images.create(
|
30
|
+
prompt: "A dog on a rocket to the moon",
|
31
|
+
response_format: "b64_json"
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "is successful" do
|
36
|
+
expect(response).to be_instance_of(LLM::Response::Image)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns an array of images" do
|
40
|
+
expect(response.images).to be_instance_of(Array)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "returns an encoded string" do
|
44
|
+
expect(response.images[0].encoded).to be_instance_of(String)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "returns an binary string" do
|
48
|
+
expect(response.images[0].binary).to be_instance_of(String)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "when given a successful variation operation",
|
53
|
+
vcr: {cassette_name: "openai/images/successful_variation"} do
|
54
|
+
subject(:response) do
|
55
|
+
provider.images.create_variation(
|
56
|
+
image: LLM::File("spec/fixtures/images/bluebook.png"),
|
57
|
+
n: 5
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "is successful" do
|
62
|
+
expect(response).to be_instance_of(LLM::Response::Image)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "returns data" do
|
66
|
+
expect(response.urls.size).to eq(5)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "returns multiple variations" do
|
70
|
+
response.urls.each { expect(_1).to be_instance_of(String) }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context "when given a successful edit",
|
75
|
+
vcr: {cassette_name: "openai/images/successful_edit"} do
|
76
|
+
subject(:response) do
|
77
|
+
provider.images.edit(
|
78
|
+
image: LLM::File("spec/fixtures/images/bluebook.png"),
|
79
|
+
prompt: "Add white background"
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "is successful" do
|
84
|
+
expect(response).to be_instance_of(LLM::Response::Image)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "returns data" do
|
88
|
+
expect(response.urls).to be_instance_of(Array)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "returns a url" do
|
92
|
+
expect(response.urls[0]).to be_instance_of(String)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|