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
data/lib/llm/providers/openai.rb
CHANGED
@@ -5,9 +5,13 @@ module LLM
|
|
5
5
|
# The OpenAI class implements a provider for
|
6
6
|
# [OpenAI](https://platform.openai.com/)
|
7
7
|
class OpenAI < Provider
|
8
|
+
require_relative "openai/format"
|
8
9
|
require_relative "openai/error_handler"
|
9
10
|
require_relative "openai/response_parser"
|
10
|
-
require_relative "openai/
|
11
|
+
require_relative "openai/responses"
|
12
|
+
require_relative "openai/images"
|
13
|
+
require_relative "openai/audio"
|
14
|
+
require_relative "openai/files"
|
11
15
|
include Format
|
12
16
|
|
13
17
|
HOST = "api.openai.com"
|
@@ -19,29 +23,71 @@ module LLM
|
|
19
23
|
end
|
20
24
|
|
21
25
|
##
|
26
|
+
# Provides an embedding
|
27
|
+
# @see https://platform.openai.com/docs/api-reference/embeddings/create OpenAI docs
|
22
28
|
# @param input (see LLM::Provider#embed)
|
29
|
+
# @param model (see LLM::Provider#embed)
|
30
|
+
# @param params (see LLM::Provider#embed)
|
31
|
+
# @raise (see LLM::Provider#request)
|
23
32
|
# @return (see LLM::Provider#embed)
|
24
|
-
def embed(input, **params)
|
33
|
+
def embed(input, model: "text-embedding-3-small", **params)
|
25
34
|
req = Net::HTTP::Post.new("/v1/embeddings", headers)
|
26
|
-
req.body = JSON.dump({input:, model:
|
35
|
+
req.body = JSON.dump({input:, model:}.merge!(params))
|
27
36
|
res = request(@http, req)
|
28
37
|
Response::Embedding.new(res).extend(response_parser)
|
29
38
|
end
|
30
39
|
|
31
40
|
##
|
41
|
+
# Provides an interface to the chat completions API
|
32
42
|
# @see https://platform.openai.com/docs/api-reference/chat/create OpenAI docs
|
33
43
|
# @param prompt (see LLM::Provider#complete)
|
34
44
|
# @param role (see LLM::Provider#complete)
|
45
|
+
# @param model (see LLM::Provider#complete)
|
46
|
+
# @param params (see LLM::Provider#complete)
|
47
|
+
# @example (see LLM::Provider#complete)
|
48
|
+
# @raise (see LLM::Provider#request)
|
35
49
|
# @return (see LLM::Provider#complete)
|
36
|
-
def complete(prompt, role = :user, **params)
|
37
|
-
params = {model:
|
50
|
+
def complete(prompt, role = :user, model: "gpt-4o-mini", **params)
|
51
|
+
params = {model:}.merge!(params)
|
38
52
|
req = Net::HTTP::Post.new("/v1/chat/completions", headers)
|
39
53
|
messages = [*(params.delete(:messages) || []), Message.new(role, prompt)]
|
40
|
-
req.body = JSON.dump({messages: format(messages)}.merge!(params))
|
54
|
+
req.body = JSON.dump({messages: format(messages, :complete)}.merge!(params))
|
41
55
|
res = request(@http, req)
|
42
56
|
Response::Completion.new(res).extend(response_parser)
|
43
57
|
end
|
44
58
|
|
59
|
+
##
|
60
|
+
# Provides an interface to OpenAI's response API
|
61
|
+
# @see https://platform.openai.com/docs/api-reference/responses/create OpenAI docs
|
62
|
+
# @return [LLM::OpenAI::Responses]
|
63
|
+
def responses
|
64
|
+
LLM::OpenAI::Responses.new(self)
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Provides an interface to OpenAI's image generation API
|
69
|
+
# @see https://platform.openai.com/docs/api-reference/images/create OpenAI docs
|
70
|
+
# @return [LLM::OpenAI::Images]
|
71
|
+
def images
|
72
|
+
LLM::OpenAI::Images.new(self)
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Provides an interface to OpenAI's audio generation API
|
77
|
+
# @see https://platform.openai.com/docs/api-reference/audio/createSpeech OpenAI docs
|
78
|
+
# @return [LLM::OpenAI::Audio]
|
79
|
+
def audio
|
80
|
+
LLM::OpenAI::Audio.new(self)
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Provides an interface to OpenAI's files API
|
85
|
+
# @see https://platform.openai.com/docs/api-reference/files/create OpenAI docs
|
86
|
+
# @return [LLM::OpenAI::Files]
|
87
|
+
def files
|
88
|
+
LLM::OpenAI::Files.new(self)
|
89
|
+
end
|
90
|
+
|
45
91
|
##
|
46
92
|
# @return (see LLM::Provider#assistant_role)
|
47
93
|
def assistant_role
|
@@ -25,7 +25,7 @@ class LLM::VoyageAI
|
|
25
25
|
when Net::HTTPTooManyRequests
|
26
26
|
raise LLM::Error::RateLimit.new { _1.response = res }, "Too many requests"
|
27
27
|
else
|
28
|
-
raise LLM::Error::
|
28
|
+
raise LLM::Error::ResponseError.new { _1.response = res }, "Unexpected response"
|
29
29
|
end
|
30
30
|
end
|
31
31
|
end
|
@@ -17,9 +17,9 @@ module LLM
|
|
17
17
|
# [Anthropic's recommendation](https://docs.anthropic.com/en/docs/build-with-claude/embeddings)
|
18
18
|
# @param input (see LLM::Provider#embed)
|
19
19
|
# @return (see LLM::Provider#embed)
|
20
|
-
def embed(input, **params)
|
20
|
+
def embed(input, model: "voyage-2", **params)
|
21
21
|
req = Net::HTTP::Post.new("/v1/embeddings", headers)
|
22
|
-
req.body = JSON.dump({input:, model:
|
22
|
+
req.body = JSON.dump({input:, model:}.merge!(params))
|
23
23
|
res = request(@http, req)
|
24
24
|
Response::Embedding.new(res).extend(response_parser)
|
25
25
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
##
|
5
|
+
# The {LLM::Response::Audio LLM::Response::Audio} class represents an
|
6
|
+
# audio file that has been returned by a provider. It wraps an IO object
|
7
|
+
# that can be used to read the contents of an audio stream (as binary data).
|
8
|
+
class Response::Audio < Response
|
9
|
+
##
|
10
|
+
# @return [StringIO]
|
11
|
+
attr_accessor :audio
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
##
|
5
|
+
# The {LLM::Response::AudioTranscription LLM::Response::AudioTranscription}
|
6
|
+
# class represents an audio transcription that has been returned by
|
7
|
+
# a provider (eg OpenAI, Gemini, etc)
|
8
|
+
class Response::AudioTranscription < Response
|
9
|
+
##
|
10
|
+
# Returns the text of the transcription
|
11
|
+
# @return [String]
|
12
|
+
attr_accessor :text
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
##
|
5
|
+
# The {LLM::Response::AudioTranslation LLM::Response::AudioTranslation}
|
6
|
+
# class represents an audio translation that has been returned by
|
7
|
+
# a provider (eg OpenAI, Gemini, etc)
|
8
|
+
class Response::AudioTranslation < Response
|
9
|
+
##
|
10
|
+
# Returns the text of the translation
|
11
|
+
# @return [String]
|
12
|
+
attr_accessor :text
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
##
|
5
|
+
# The {LLM::Response::DownloadFile LLM::Response::DownloadFile} class
|
6
|
+
# represents the contents of a file that has been returned by a
|
7
|
+
# provider. It wraps an IO object that can be used to read the file
|
8
|
+
# contents.
|
9
|
+
class Response::DownloadFile < Response
|
10
|
+
##
|
11
|
+
# Returns a StringIO object
|
12
|
+
# @return [StringIO]
|
13
|
+
attr_accessor :file
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
##
|
5
|
+
# The {LLM::Response::File LLM::Response::File} class represents a file
|
6
|
+
# that has been uploaded to a provider. Its properties are delegated
|
7
|
+
# to the underlying response body, and vary by provider.
|
8
|
+
class Response::File < Response
|
9
|
+
##
|
10
|
+
# Returns a normalized response body
|
11
|
+
# @return [Hash]
|
12
|
+
def body
|
13
|
+
@_body ||= if super["file"]
|
14
|
+
super["file"].transform_keys { snakecase(_1) }
|
15
|
+
else
|
16
|
+
super.transform_keys { snakecase(_1) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# @return [String]
|
22
|
+
def inspect
|
23
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} body=#{body}>"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
include LLM::Utils
|
29
|
+
|
30
|
+
def respond_to_missing?(m, _)
|
31
|
+
body.key?(m.to_s) || super
|
32
|
+
end
|
33
|
+
|
34
|
+
def method_missing(m, *args, &block)
|
35
|
+
if body.key?(m.to_s)
|
36
|
+
body[m.to_s]
|
37
|
+
else
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
##
|
5
|
+
# The {LLM::Response::FileList LLM::Response::FileList} class represents a
|
6
|
+
# list of file objects that are returned by a provider. It is an Enumerable
|
7
|
+
# object, and can be used to iterate over the file objects in a way that is
|
8
|
+
# similar to an array. Each element is an instance of OpenStruct.
|
9
|
+
class Response::FileList < Response
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
attr_accessor :files
|
13
|
+
|
14
|
+
def each(&)
|
15
|
+
@files.each(&)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
##
|
5
|
+
# The {LLM::Response::Image LLM::Response::Image} class represents
|
6
|
+
# an image response. An image response might encapsulate one or more
|
7
|
+
# URLs, or a base64 encoded image -- depending on the provider.
|
8
|
+
class Response::Image < Response
|
9
|
+
##
|
10
|
+
# Returns one or more image objects, or nil
|
11
|
+
# @return [Array<OpenStruct>, nil]
|
12
|
+
def images
|
13
|
+
parsed[:images].any? ? parsed[:images] : nil
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Returns one or more image URLs, or nil
|
18
|
+
# @return [Array<String>, nil]
|
19
|
+
def urls
|
20
|
+
parsed[:urls].any? ? parsed[:urls] : nil
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def parsed
|
26
|
+
@parsed ||= parse_image(body)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
class Response::Output < Response
|
5
|
+
##
|
6
|
+
# @return [String]
|
7
|
+
# Returns the id of the response
|
8
|
+
def id
|
9
|
+
parsed[:id]
|
10
|
+
end
|
11
|
+
|
12
|
+
##
|
13
|
+
# @return [String]
|
14
|
+
# Returns the model name
|
15
|
+
def model
|
16
|
+
parsed[:model]
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# @return [Array<LLM::Message>]
|
21
|
+
def outputs
|
22
|
+
parsed[:outputs]
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# @return [Integer]
|
27
|
+
# Returns the input token count
|
28
|
+
def input_tokens
|
29
|
+
parsed[:input_tokens]
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# @return [Integer]
|
34
|
+
# Returns the output token count
|
35
|
+
def output_tokens
|
36
|
+
parsed[:output_tokens]
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# @return [Integer]
|
41
|
+
# Returns the total count of tokens
|
42
|
+
def total_tokens
|
43
|
+
parsed[:total_tokens]
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
##
|
49
|
+
# @private
|
50
|
+
# @return [Hash]
|
51
|
+
# Returns the parsed response from the provider
|
52
|
+
def parsed
|
53
|
+
@parsed ||= parse_output_response(body)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/llm/response.rb
CHANGED
@@ -5,11 +5,14 @@ module LLM
|
|
5
5
|
require "json"
|
6
6
|
require_relative "response/completion"
|
7
7
|
require_relative "response/embedding"
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
require_relative "response/output"
|
9
|
+
require_relative "response/image"
|
10
|
+
require_relative "response/audio"
|
11
|
+
require_relative "response/audio_transcription"
|
12
|
+
require_relative "response/audio_translation"
|
13
|
+
require_relative "response/file"
|
14
|
+
require_relative "response/filelist"
|
15
|
+
require_relative "response/download_file"
|
13
16
|
|
14
17
|
##
|
15
18
|
# @param [Net::HTTPResponse] res
|
@@ -18,7 +21,16 @@ module LLM
|
|
18
21
|
# Returns an instance of LLM::Response
|
19
22
|
def initialize(res)
|
20
23
|
@res = res
|
21
|
-
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Returns the response body
|
28
|
+
# @return [Hash, String]
|
29
|
+
def body
|
30
|
+
@body ||= case @res["content-type"]
|
31
|
+
when %r|\Aapplication/json\s*| then JSON.parse(@res.body)
|
32
|
+
else @res.body
|
33
|
+
end
|
22
34
|
end
|
23
35
|
end
|
24
36
|
end
|
data/lib/llm/utils.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# @private
|
5
|
+
module LLM::Utils
|
6
|
+
def camelcase(key)
|
7
|
+
key.to_s
|
8
|
+
.split("_")
|
9
|
+
.map.with_index { (_2 > 0) ? _1.capitalize : _1 }
|
10
|
+
.join
|
11
|
+
end
|
12
|
+
|
13
|
+
def snakecase(key)
|
14
|
+
key
|
15
|
+
.split(/([A-Z])/)
|
16
|
+
.map { (_1.size == 1) ? "_#{_1.downcase}" : _1 }
|
17
|
+
.join
|
18
|
+
end
|
19
|
+
end
|
data/lib/llm/version.rb
CHANGED
data/lib/llm.rb
CHANGED
@@ -2,14 +2,17 @@
|
|
2
2
|
|
3
3
|
module LLM
|
4
4
|
require_relative "llm/version"
|
5
|
+
require_relative "llm/utils"
|
5
6
|
require_relative "llm/error"
|
6
7
|
require_relative "llm/message"
|
7
8
|
require_relative "llm/response"
|
9
|
+
require_relative "llm/mime"
|
10
|
+
require_relative "llm/multipart"
|
8
11
|
require_relative "llm/file"
|
9
12
|
require_relative "llm/model"
|
10
13
|
require_relative "llm/provider"
|
11
|
-
require_relative "llm/
|
12
|
-
require_relative "llm/
|
14
|
+
require_relative "llm/chat"
|
15
|
+
require_relative "llm/buffer"
|
13
16
|
require_relative "llm/core_ext/ostruct"
|
14
17
|
|
15
18
|
module_function
|
data/llm.gemspec
CHANGED
@@ -14,12 +14,11 @@ Gem::Specification.new do |spec|
|
|
14
14
|
"flexible, and easy to use."
|
15
15
|
spec.description = spec.summary
|
16
16
|
spec.homepage = "https://github.com/llmrb/llm"
|
17
|
-
spec.license = "
|
17
|
+
spec.license = "0BSDL"
|
18
18
|
spec.required_ruby_version = ">= 3.0.0"
|
19
19
|
|
20
20
|
spec.metadata["homepage_uri"] = spec.homepage
|
21
21
|
spec.metadata["source_code_uri"] = "https://github.com/llmrb/llm"
|
22
|
-
spec.metadata["changelog_uri"] = "https://github.com/llmrb/llm/blob/main/CHANGELOG.md"
|
23
22
|
|
24
23
|
spec.files = Dir[
|
25
24
|
"README.md", "LICENSE.txt",
|
@@ -29,10 +28,6 @@ Gem::Specification.new do |spec|
|
|
29
28
|
]
|
30
29
|
spec.require_paths = ["lib"]
|
31
30
|
|
32
|
-
spec.add_runtime_dependency "net-http", "~> 0.6.0"
|
33
|
-
spec.add_runtime_dependency "json"
|
34
|
-
spec.add_runtime_dependency "yaml"
|
35
|
-
|
36
31
|
spec.add_development_dependency "webmock", "~> 3.24.0"
|
37
32
|
spec.add_development_dependency "yard", "~> 0.9.37"
|
38
33
|
spec.add_development_dependency "kramdown", "~> 2.4"
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "setup"
|
4
|
+
|
5
|
+
RSpec.describe "LLM::Chat: gemini" do
|
6
|
+
let(:described_class) { LLM::Chat }
|
7
|
+
let(:provider) { LLM.gemini(token) }
|
8
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
9
|
+
let(:conversation) { described_class.new(provider, **params).lazy }
|
10
|
+
|
11
|
+
context "when asked to describe an image",
|
12
|
+
vcr: {cassette_name: "gemini/conversations/multimodal_response"} do
|
13
|
+
subject { conversation.last_message }
|
14
|
+
|
15
|
+
let(:params) { {} }
|
16
|
+
let(:image) { LLM::File("spec/fixtures/images/bluebook.png") }
|
17
|
+
|
18
|
+
before do
|
19
|
+
conversation.chat(image, :user)
|
20
|
+
conversation.chat("Describe the image with a short sentance", :user)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "describes the image" do
|
24
|
+
is_expected.to have_attributes(
|
25
|
+
role: "model",
|
26
|
+
content: "That's a simple illustration of a book " \
|
27
|
+
"resting on a blue, X-shaped book stand.\n"
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "setup"
|
4
|
+
|
5
|
+
RSpec.describe "LLM::Gemini::Files" do
|
6
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
7
|
+
let(:provider) { LLM.gemini(token) }
|
8
|
+
|
9
|
+
context "when given a successful create operation (bismillah.mp3)",
|
10
|
+
vcr: {cassette_name: "gemini/files/successful_create_bismillah"} do
|
11
|
+
subject(:file) { provider.files.create(file: LLM::File("spec/fixtures/audio/bismillah.mp3")) }
|
12
|
+
after { provider.files.delete(file:) }
|
13
|
+
|
14
|
+
it "is successful" do
|
15
|
+
expect(file).to be_instance_of(LLM::Response::File)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "returns a file object" do
|
19
|
+
expect(file).to have_attributes(
|
20
|
+
name: instance_of(String),
|
21
|
+
display_name: "bismillah.mp3"
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when given a successful delete operation (bismillah.mp3)",
|
27
|
+
vcr: {cassette_name: "gemini/files/successful_delete_bismillah"} do
|
28
|
+
let(:file) { provider.files.create(file: LLM::File("spec/fixtures/audio/bismillah.mp3")) }
|
29
|
+
subject { provider.files.delete(file:) }
|
30
|
+
|
31
|
+
it "is successful" do
|
32
|
+
is_expected.to be_instance_of(Net::HTTPOK)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "when given a successful get operation (bismillah.mp3)",
|
37
|
+
vcr: {cassette_name: "gemini/files/successful_get_bismillah"} do
|
38
|
+
let(:file) { provider.files.create(file: LLM::File("spec/fixtures/audio/bismillah.mp3")) }
|
39
|
+
subject { provider.files.get(file:) }
|
40
|
+
after { provider.files.delete(file:) }
|
41
|
+
|
42
|
+
it "is successful" do
|
43
|
+
is_expected.to be_instance_of(LLM::Response::File)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns a file object" do
|
47
|
+
is_expected.to have_attributes(
|
48
|
+
name: instance_of(String),
|
49
|
+
display_name: "bismillah.mp3"
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "when given a successful translation operation (bismillah.mp3)",
|
55
|
+
vcr: {cassette_name: "gemini/files/successful_translation_bismillah"} do
|
56
|
+
subject { bot.last_message.content }
|
57
|
+
let(:file) { provider.files.create(file: LLM::File("spec/fixtures/audio/bismillah.mp3")) }
|
58
|
+
let(:bot) { LLM::Chat.new(provider).lazy }
|
59
|
+
after { provider.files.delete(file:) }
|
60
|
+
|
61
|
+
before do
|
62
|
+
bot.chat file
|
63
|
+
bot.chat "Translate the contents of the audio file into English"
|
64
|
+
bot.chat "The audio is referenced in the first message I sent to you"
|
65
|
+
bot.chat "Provide no other content except the translation"
|
66
|
+
end
|
67
|
+
|
68
|
+
it "translates the audio clip" do
|
69
|
+
is_expected.to eq("In the name of Allah, the Most Gracious, the Most Merciful.\n")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "when given a successful translation operation (alhamdullilah.mp3)",
|
74
|
+
vcr: {cassette_name: "gemini/files/successful_translation_alhamdullilah"} do
|
75
|
+
subject { bot.last_message.content }
|
76
|
+
let(:file) { provider.files.create(file: LLM::File("spec/fixtures/audio/alhamdullilah.mp3")) }
|
77
|
+
let(:bot) { LLM::Chat.new(provider).lazy }
|
78
|
+
after { provider.files.delete(file:) }
|
79
|
+
|
80
|
+
before do
|
81
|
+
bot.chat [
|
82
|
+
"Translate the contents of the audio file into English",
|
83
|
+
"Provide no other content except the translation",
|
84
|
+
file
|
85
|
+
]
|
86
|
+
end
|
87
|
+
|
88
|
+
it "translates the audio clip" do
|
89
|
+
is_expected.to eq("All praise is due to Allah, Lord of the worlds.\n")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "when given a successful all operation",
|
94
|
+
vcr: {cassette_name: "gemini/files/successful_all"} do
|
95
|
+
let!(:files) do
|
96
|
+
[
|
97
|
+
provider.files.create(file: LLM::File("spec/fixtures/audio/bismillah.mp3")),
|
98
|
+
provider.files.create(file: LLM::File("spec/fixtures/audio/alhamdullilah.mp3"))
|
99
|
+
]
|
100
|
+
end
|
101
|
+
|
102
|
+
subject(:response) { provider.files.all }
|
103
|
+
after { files.each { |file| provider.files.delete(file:) } }
|
104
|
+
|
105
|
+
it "is successful" do
|
106
|
+
expect(response).to be_instance_of(LLM::Response::FileList)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "returns an array of file objects" do
|
110
|
+
expect(response).to match_array(
|
111
|
+
[
|
112
|
+
have_attributes(
|
113
|
+
name: instance_of(String),
|
114
|
+
display_name: "bismillah.mp3"
|
115
|
+
),
|
116
|
+
have_attributes(
|
117
|
+
name: instance_of(String),
|
118
|
+
display_name: "alhamdullilah.mp3"
|
119
|
+
)
|
120
|
+
]
|
121
|
+
)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "setup"
|
4
|
+
|
5
|
+
RSpec.describe "LLM::Gemini::Images" do
|
6
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
7
|
+
let(:provider) { LLM.gemini(token) }
|
8
|
+
|
9
|
+
context "when given a successful create operation",
|
10
|
+
vcr: {cassette_name: "gemini/images/successful_create"} 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 encoded string" do
|
18
|
+
expect(response.images[0].encoded).to be_instance_of(String)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns a binary string" do
|
22
|
+
expect(response.images[0].binary).to be_instance_of(String)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when given a successful edit operation",
|
27
|
+
vcr: {cassette_name: "gemini/images/successful_edit"} do
|
28
|
+
subject(:response) do
|
29
|
+
provider.images.edit(
|
30
|
+
image: LLM::File("spec/fixtures/images/bluebook.png"),
|
31
|
+
prompt: "Book is floating in the clouds"
|
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 data" do
|
40
|
+
expect(response.images[0].encoded).to be_instance_of(String)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "returns a url" do
|
44
|
+
expect(response.images[0].binary).to be_instance_of(String)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|