llm.rb 0.10.1 → 0.11.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/LICENSE +0 -0
- data/README.md +81 -117
- data/lib/llm/bot/builder.rb +2 -2
- data/lib/llm/bot/conversable.rb +0 -0
- data/lib/llm/bot/prompt/completion.rb +0 -0
- data/lib/llm/bot/prompt/respond.rb +0 -0
- data/lib/llm/bot.rb +9 -11
- data/lib/llm/buffer.rb +0 -0
- data/lib/llm/error.rb +0 -0
- data/lib/llm/event_handler.rb +0 -0
- data/lib/llm/eventstream/event.rb +0 -0
- data/lib/llm/eventstream/parser.rb +0 -0
- data/lib/llm/eventstream.rb +0 -0
- data/lib/llm/file.rb +18 -9
- data/lib/llm/function.rb +6 -5
- data/lib/llm/json/schema/array.rb +0 -0
- data/lib/llm/json/schema/boolean.rb +0 -0
- data/lib/llm/json/schema/integer.rb +0 -0
- data/lib/llm/json/schema/leaf.rb +0 -0
- data/lib/llm/json/schema/null.rb +0 -0
- data/lib/llm/json/schema/number.rb +0 -0
- data/lib/llm/json/schema/object.rb +0 -0
- data/lib/llm/json/schema/string.rb +0 -0
- data/lib/llm/json/schema/version.rb +0 -0
- data/lib/llm/json/schema.rb +0 -0
- data/lib/llm/message.rb +8 -0
- data/lib/llm/mime.rb +0 -0
- data/lib/llm/multipart.rb +0 -0
- data/lib/llm/object/builder.rb +0 -0
- data/lib/llm/object/kernel.rb +8 -0
- data/lib/llm/object.rb +7 -0
- data/lib/llm/provider.rb +9 -11
- data/lib/llm/providers/anthropic/error_handler.rb +0 -0
- data/lib/llm/providers/anthropic/format/completion_format.rb +10 -5
- data/lib/llm/providers/anthropic/format.rb +0 -0
- data/lib/llm/providers/anthropic/models.rb +2 -7
- data/lib/llm/providers/anthropic/response/completion.rb +39 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +0 -0
- data/lib/llm/providers/anthropic.rb +3 -24
- data/lib/llm/providers/deepseek/format/completion_format.rb +3 -3
- data/lib/llm/providers/deepseek/format.rb +0 -0
- data/lib/llm/providers/deepseek.rb +6 -0
- data/lib/llm/providers/gemini/audio.rb +6 -10
- data/lib/llm/providers/gemini/error_handler.rb +0 -0
- data/lib/llm/providers/gemini/files.rb +11 -14
- data/lib/llm/providers/gemini/format/completion_format.rb +20 -5
- data/lib/llm/providers/gemini/format.rb +0 -0
- data/lib/llm/providers/gemini/images.rb +8 -7
- data/lib/llm/providers/gemini/models.rb +2 -8
- data/lib/llm/providers/gemini/{response_parser/completion_parser.rb → response/completion.rb} +10 -24
- data/lib/llm/providers/gemini/response/embedding.rb +8 -0
- data/lib/llm/providers/gemini/response/file.rb +11 -0
- data/lib/llm/providers/gemini/response/image.rb +26 -0
- data/lib/llm/providers/gemini/stream_parser.rb +0 -0
- data/lib/llm/providers/gemini.rb +5 -8
- data/lib/llm/providers/llamacpp.rb +6 -0
- data/lib/llm/providers/ollama/error_handler.rb +0 -0
- data/lib/llm/providers/ollama/format/completion_format.rb +8 -5
- data/lib/llm/providers/ollama/format.rb +0 -0
- data/lib/llm/providers/ollama/models.rb +2 -8
- data/lib/llm/providers/ollama/response/completion.rb +28 -0
- data/lib/llm/providers/ollama/response/embedding.rb +10 -0
- data/lib/llm/providers/ollama/stream_parser.rb +0 -0
- data/lib/llm/providers/ollama.rb +5 -8
- data/lib/llm/providers/openai/audio.rb +6 -6
- data/lib/llm/providers/openai/error_handler.rb +0 -0
- data/lib/llm/providers/openai/files.rb +14 -15
- data/lib/llm/providers/openai/format/completion_format.rb +11 -4
- data/lib/llm/providers/openai/format/moderation_format.rb +2 -2
- data/lib/llm/providers/openai/format/respond_format.rb +7 -4
- data/lib/llm/providers/openai/format.rb +0 -0
- data/lib/llm/providers/openai/images.rb +8 -7
- data/lib/llm/providers/openai/models.rb +2 -7
- data/lib/llm/providers/openai/moderations.rb +9 -11
- data/lib/llm/providers/openai/response/audio.rb +7 -0
- data/lib/llm/providers/openai/{response_parser/completion_parser.rb → response/completion.rb} +15 -31
- data/lib/llm/providers/openai/response/embedding.rb +9 -0
- data/lib/llm/providers/openai/response/file.rb +7 -0
- data/lib/llm/providers/openai/response/image.rb +16 -0
- data/lib/llm/providers/openai/response/moderations.rb +34 -0
- data/lib/llm/providers/openai/{response_parser/respond_parser.rb → response/responds.rb} +7 -28
- data/lib/llm/providers/openai/responses.rb +10 -9
- data/lib/llm/providers/openai/stream_parser.rb +0 -0
- data/lib/llm/providers/openai/vector_stores.rb +106 -0
- data/lib/llm/providers/openai.rb +14 -8
- data/lib/llm/response.rb +37 -13
- data/lib/llm/utils.rb +0 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +2 -12
- data/llm.gemspec +1 -1
- metadata +18 -29
- data/lib/llm/model.rb +0 -32
- data/lib/llm/providers/anthropic/response_parser/completion_parser.rb +0 -51
- data/lib/llm/providers/anthropic/response_parser.rb +0 -24
- data/lib/llm/providers/gemini/response_parser.rb +0 -46
- data/lib/llm/providers/ollama/response_parser/completion_parser.rb +0 -42
- data/lib/llm/providers/ollama/response_parser.rb +0 -30
- data/lib/llm/providers/openai/response_parser.rb +0 -65
- data/lib/llm/providers/voyageai/error_handler.rb +0 -32
- data/lib/llm/providers/voyageai/response_parser.rb +0 -13
- data/lib/llm/providers/voyageai.rb +0 -44
- data/lib/llm/response/audio.rb +0 -13
- data/lib/llm/response/audio_transcription.rb +0 -14
- data/lib/llm/response/audio_translation.rb +0 -14
- data/lib/llm/response/completion.rb +0 -51
- data/lib/llm/response/download_file.rb +0 -15
- data/lib/llm/response/embedding.rb +0 -23
- data/lib/llm/response/file.rb +0 -42
- data/lib/llm/response/filelist.rb +0 -18
- data/lib/llm/response/image.rb +0 -29
- data/lib/llm/response/modellist.rb +0 -18
- data/lib/llm/response/moderationlist/moderation.rb +0 -47
- data/lib/llm/response/moderationlist.rb +0 -51
- data/lib/llm/response/respond.rb +0 -56
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM::Anthropic::Response
|
4
|
+
module Completion
|
5
|
+
def choices = format_choices
|
6
|
+
def role = body.role
|
7
|
+
def model = body.model
|
8
|
+
def prompt_tokens = body.usage&.input_tokens || 0
|
9
|
+
def completion_tokens = body.usage&.output_tokens || 0
|
10
|
+
def total_tokens = prompt_tokens + completion_tokens
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def format_choices
|
15
|
+
texts.map.with_index do |choice, index|
|
16
|
+
extra = {
|
17
|
+
index:, response: self,
|
18
|
+
tool_calls: format_tool_calls(tools), original_tool_calls: tools
|
19
|
+
}
|
20
|
+
LLM::Message.new(role, choice["text"], extra)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def format_tool_calls(tools)
|
25
|
+
(tools || []).filter_map do |tool|
|
26
|
+
tool = {
|
27
|
+
id: tool.id,
|
28
|
+
name: tool.name,
|
29
|
+
arguments: tool.input
|
30
|
+
}
|
31
|
+
LLM::Object.new(tool)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def parts = body.content
|
36
|
+
def texts = @texts ||= LLM::Object.from_hash(parts.select { _1["type"] == "text" })
|
37
|
+
def tools = @tools ||= LLM::Object.from_hash(parts.select { _1["type"] == "tool_use" })
|
38
|
+
end
|
39
|
+
end
|
File without changes
|
@@ -5,10 +5,10 @@ module LLM
|
|
5
5
|
# The Anthropic class implements a provider for
|
6
6
|
# [Anthropic](https://www.anthropic.com)
|
7
7
|
class Anthropic < Provider
|
8
|
+
require_relative "anthropic/response/completion"
|
8
9
|
require_relative "anthropic/format"
|
9
10
|
require_relative "anthropic/error_handler"
|
10
11
|
require_relative "anthropic/stream_parser"
|
11
|
-
require_relative "anthropic/response_parser"
|
12
12
|
require_relative "anthropic/models"
|
13
13
|
include Format
|
14
14
|
|
@@ -20,23 +20,6 @@ module LLM
|
|
20
20
|
super(host: HOST, **)
|
21
21
|
end
|
22
22
|
|
23
|
-
##
|
24
|
-
# Provides an embedding via VoyageAI per
|
25
|
-
# [Anthropic's recommendation](https://docs.anthropic.com/en/docs/build-with-claude/embeddings)
|
26
|
-
# @param input (see LLM::Provider#embed)
|
27
|
-
# @param [String] key
|
28
|
-
# Valid key for the VoyageAI API
|
29
|
-
# @param [String] model
|
30
|
-
# The embedding model to use
|
31
|
-
# @param [Hash] params
|
32
|
-
# Other embedding parameters
|
33
|
-
# @raise (see LLM::Provider#request)
|
34
|
-
# @return (see LLM::Provider#embed)
|
35
|
-
def embed(input, key:, model: "voyage-2", **params)
|
36
|
-
llm = LLM.voyageai(key:)
|
37
|
-
llm.embed(input, **params.merge(model:))
|
38
|
-
end
|
39
|
-
|
40
23
|
##
|
41
24
|
# Provides an interface to the chat completions API
|
42
25
|
# @see https://docs.anthropic.com/en/api/messages Anthropic docs
|
@@ -44,7 +27,7 @@ module LLM
|
|
44
27
|
# @param params (see LLM::Provider#complete)
|
45
28
|
# @example (see LLM::Provider#complete)
|
46
29
|
# @raise (see LLM::Provider#request)
|
47
|
-
# @raise [LLM::
|
30
|
+
# @raise [LLM::PromptError]
|
48
31
|
# When given an object a provider does not understand
|
49
32
|
# @return (see LLM::Provider#complete)
|
50
33
|
def complete(prompt, params = {})
|
@@ -57,7 +40,7 @@ module LLM
|
|
57
40
|
body = JSON.dump({messages: [format(messages)].flatten}.merge!(params))
|
58
41
|
set_body_stream(req, StringIO.new(body))
|
59
42
|
res = execute(request: req, stream:)
|
60
|
-
Response
|
43
|
+
LLM::Response.new(res).extend(LLM::Anthropic::Response::Completion)
|
61
44
|
end
|
62
45
|
|
63
46
|
##
|
@@ -92,10 +75,6 @@ module LLM
|
|
92
75
|
)
|
93
76
|
end
|
94
77
|
|
95
|
-
def response_parser
|
96
|
-
LLM::Anthropic::ResponseParser
|
97
|
-
end
|
98
|
-
|
99
78
|
def stream_parser
|
100
79
|
LLM::Anthropic::StreamParser
|
101
80
|
end
|
@@ -12,7 +12,7 @@ module LLM::DeepSeek::Format
|
|
12
12
|
end
|
13
13
|
|
14
14
|
##
|
15
|
-
# Formats the message for the
|
15
|
+
# Formats the message for the DeepSeek chat completions API
|
16
16
|
# @return [Hash]
|
17
17
|
def format
|
18
18
|
catch(:abort) do
|
@@ -37,8 +37,8 @@ module LLM::DeepSeek::Format
|
|
37
37
|
when LLM::Function::Return
|
38
38
|
throw(:abort, {role: "tool", tool_call_id: content.id, content: JSON.dump(content.value)})
|
39
39
|
else
|
40
|
-
raise LLM::
|
41
|
-
|
40
|
+
raise LLM::PromptError, "The given object (an instance of #{content.class}) " \
|
41
|
+
"is not supported by the DeepSeek chat completions API"
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
File without changes
|
@@ -49,6 +49,12 @@ module LLM
|
|
49
49
|
raise NotImplementedError
|
50
50
|
end
|
51
51
|
|
52
|
+
##
|
53
|
+
# @raise [NotImplementedError]
|
54
|
+
def vector_stores
|
55
|
+
raise NotImplementedError
|
56
|
+
end
|
57
|
+
|
52
58
|
##
|
53
59
|
# Returns the default model for chat completions
|
54
60
|
# @see https://api-docs.deepseek.com/quick_start/pricing deepseek-chat
|
@@ -34,20 +34,18 @@ class LLM::Gemini
|
|
34
34
|
# res = llm.audio.create_transcription(file: "/audio/rocket.mp3")
|
35
35
|
# res.text # => "A dog on a rocket to the moon"
|
36
36
|
# @see https://ai.google.dev/gemini-api/docs/audio Gemini docs
|
37
|
-
# @param [String, LLM::File, LLM::Response
|
37
|
+
# @param [String, LLM::File, LLM::Response] file The input audio
|
38
38
|
# @param [String] model The model to use
|
39
39
|
# @param [Hash] params Other parameters (see Gemini docs)
|
40
40
|
# @raise (see LLM::Provider#request)
|
41
|
-
# @return [LLM::Response
|
41
|
+
# @return [LLM::Response]
|
42
42
|
def create_transcription(file:, model: "gemini-1.5-flash", **params)
|
43
43
|
res = @provider.complete [
|
44
44
|
"Your task is to transcribe the contents of an audio file",
|
45
45
|
"Your response should include the transcription, and nothing else",
|
46
46
|
LLM.File(file)
|
47
47
|
], params.merge(role: :user, model:)
|
48
|
-
|
49
|
-
.new(res)
|
50
|
-
.tap { _1.text = res.choices[0].content }
|
48
|
+
res.tap { _1.define_singleton_method(:text) { choices[0].content } }
|
51
49
|
end
|
52
50
|
|
53
51
|
##
|
@@ -58,20 +56,18 @@ class LLM::Gemini
|
|
58
56
|
# res = llm.audio.create_translation(file: "/audio/bismillah.mp3")
|
59
57
|
# res.text # => "In the name of Allah, the Beneficent, the Merciful."
|
60
58
|
# @see https://ai.google.dev/gemini-api/docs/audio Gemini docs
|
61
|
-
# @param [String, LLM::File, LLM::Response
|
59
|
+
# @param [String, LLM::File, LLM::Response] file The input audio
|
62
60
|
# @param [String] model The model to use
|
63
61
|
# @param [Hash] params Other parameters (see Gemini docs)
|
64
62
|
# @raise (see LLM::Provider#request)
|
65
|
-
# @return [LLM::Response
|
63
|
+
# @return [LLM::Response]
|
66
64
|
def create_translation(file:, model: "gemini-1.5-flash", **params)
|
67
65
|
res = @provider.complete [
|
68
66
|
"Your task is to translate the contents of an audio file into English",
|
69
67
|
"Your response should include the translation, and nothing else",
|
70
68
|
LLM.File(file)
|
71
69
|
], params.merge(role: :user, model:)
|
72
|
-
|
73
|
-
.new(res)
|
74
|
-
.tap { _1.text = res.choices[0].content }
|
70
|
+
res.tap { _1.define_singleton_method(:text) { choices[0].content } }
|
75
71
|
end
|
76
72
|
end
|
77
73
|
end
|
File without changes
|
@@ -35,6 +35,8 @@ class LLM::Gemini
|
|
35
35
|
# bot.chat(["Describe the audio file I sent to you", file])
|
36
36
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
37
37
|
class Files
|
38
|
+
require_relative "response/file"
|
39
|
+
|
38
40
|
##
|
39
41
|
# Returns a new Files object
|
40
42
|
# @param provider [LLM::Provider]
|
@@ -54,18 +56,12 @@ class LLM::Gemini
|
|
54
56
|
# @see https://ai.google.dev/gemini-api/docs/files Gemini docs
|
55
57
|
# @param [Hash] params Other parameters (see Gemini docs)
|
56
58
|
# @raise (see LLM::Provider#request)
|
57
|
-
# @return [LLM::Response
|
59
|
+
# @return [LLM::Response]
|
58
60
|
def all(**params)
|
59
61
|
query = URI.encode_www_form(params.merge!(key: key))
|
60
62
|
req = Net::HTTP::Get.new("/v1beta/files?#{query}", headers)
|
61
63
|
res = execute(request: req)
|
62
|
-
LLM::Response
|
63
|
-
files = filelist.body["files"]&.map do |file|
|
64
|
-
file = file.transform_keys { snakecase(_1) }
|
65
|
-
LLM::Object.from_hash(file)
|
66
|
-
end || []
|
67
|
-
filelist.files = files
|
68
|
-
}
|
64
|
+
LLM::Response.new(res)
|
69
65
|
end
|
70
66
|
|
71
67
|
##
|
@@ -77,7 +73,7 @@ class LLM::Gemini
|
|
77
73
|
# @param [String, LLM::File] file The file
|
78
74
|
# @param [Hash] params Other parameters (see Gemini docs)
|
79
75
|
# @raise (see LLM::Provider#request)
|
80
|
-
# @return [LLM::Response
|
76
|
+
# @return [LLM::Response]
|
81
77
|
def create(file:, **params)
|
82
78
|
file = LLM.File(file)
|
83
79
|
req = Net::HTTP::Post.new(request_upload_url(file:), {})
|
@@ -87,7 +83,7 @@ class LLM::Gemini
|
|
87
83
|
file.with_io do |io|
|
88
84
|
set_body_stream(req, io)
|
89
85
|
res = execute(request: req)
|
90
|
-
LLM::Response
|
86
|
+
LLM::Response.new(res).extend(LLM::Gemini::Response::File)
|
91
87
|
end
|
92
88
|
end
|
93
89
|
|
@@ -101,13 +97,13 @@ class LLM::Gemini
|
|
101
97
|
# @param [#name, String] file The file to get
|
102
98
|
# @param [Hash] params Other parameters (see Gemini docs)
|
103
99
|
# @raise (see LLM::Provider#request)
|
104
|
-
# @return [LLM::Response
|
100
|
+
# @return [LLM::Response]
|
105
101
|
def get(file:, **params)
|
106
102
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
107
103
|
query = URI.encode_www_form(params.merge!(key: key))
|
108
104
|
req = Net::HTTP::Get.new("/v1beta/#{file_id}?#{query}", headers)
|
109
105
|
res = execute(request: req)
|
110
|
-
LLM::Response
|
106
|
+
LLM::Response.new(res).extend(LLM::Gemini::Response::File)
|
111
107
|
end
|
112
108
|
|
113
109
|
##
|
@@ -119,12 +115,13 @@ class LLM::Gemini
|
|
119
115
|
# @param [#name, String] file The file to delete
|
120
116
|
# @param [Hash] params Other parameters (see Gemini docs)
|
121
117
|
# @raise (see LLM::Provider#request)
|
122
|
-
# @return [LLM::Response
|
118
|
+
# @return [LLM::Response]
|
123
119
|
def delete(file:, **params)
|
124
120
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
125
121
|
query = URI.encode_www_form(params.merge!(key: key))
|
126
122
|
req = Net::HTTP::Delete.new("/v1beta/#{file_id}?#{query}", headers)
|
127
|
-
execute(request: req)
|
123
|
+
res = execute(request: req)
|
124
|
+
LLM::Response.new(res)
|
128
125
|
end
|
129
126
|
|
130
127
|
##
|
@@ -30,9 +30,11 @@ module LLM::Gemini::Format
|
|
30
30
|
case content
|
31
31
|
when Array
|
32
32
|
content.empty? ? throw(:abort, nil) : content.flat_map { format_content(_1) }
|
33
|
-
when LLM::Response
|
34
|
-
|
35
|
-
|
33
|
+
when LLM::Response
|
34
|
+
format_response(content)
|
35
|
+
when File
|
36
|
+
content.close unless content.closed?
|
37
|
+
format_content(LLM.File(content.path))
|
36
38
|
when LLM::File
|
37
39
|
file = content
|
38
40
|
[{inline_data: {mime_type: file.mime_type, data: file.to_b64}}]
|
@@ -43,11 +45,24 @@ module LLM::Gemini::Format
|
|
43
45
|
when LLM::Function::Return
|
44
46
|
[{text: JSON.dump(content.value)}]
|
45
47
|
else
|
46
|
-
|
47
|
-
"is not supported by the Gemini API"
|
48
|
+
prompt_error!(content)
|
48
49
|
end
|
49
50
|
end
|
50
51
|
|
52
|
+
def format_response(response)
|
53
|
+
if response.file?
|
54
|
+
file = response
|
55
|
+
[{file_data: {mime_type: file.mime_type, file_uri: file.uri}}]
|
56
|
+
else
|
57
|
+
prompt_error!(content)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def prompt_error!(object)
|
62
|
+
raise LLM::PromptError, "The given object (an instance of #{object.class}) " \
|
63
|
+
"is not supported by the Gemini API"
|
64
|
+
end
|
65
|
+
|
51
66
|
def message = @message
|
52
67
|
def content = message.content
|
53
68
|
end
|
File without changes
|
@@ -15,6 +15,7 @@ class LLM::Gemini
|
|
15
15
|
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
16
16
|
# IO.copy_stream res.images[0], "rocket.png"
|
17
17
|
class Images
|
18
|
+
require_relative "response/image"
|
18
19
|
include Format
|
19
20
|
|
20
21
|
##
|
@@ -39,16 +40,16 @@ class LLM::Gemini
|
|
39
40
|
# The prompt should make it clear you want to generate an image, or you
|
40
41
|
# might unexpectedly receive a purely textual response. This is due to how
|
41
42
|
# Gemini implements image generation under the hood.
|
42
|
-
# @return [LLM::Response
|
43
|
+
# @return [LLM::Response]
|
43
44
|
def create(prompt:, model: "gemini-2.0-flash-exp-image-generation", **params)
|
44
45
|
req = Net::HTTP::Post.new("/v1beta/models/#{model}:generateContent?key=#{key}", headers)
|
45
46
|
body = JSON.dump({
|
46
|
-
contents: [{parts: [{text:
|
47
|
+
contents: [{parts: [{text: system_prompt}, {text: prompt}]}],
|
47
48
|
generationConfig: {responseModalities: ["TEXT", "IMAGE"]}
|
48
49
|
}.merge!(params))
|
49
50
|
req.body = body
|
50
51
|
res = execute(request: req)
|
51
|
-
LLM::Response
|
52
|
+
LLM::Response.new(res).extend(LLM::Gemini::Response::Image)
|
52
53
|
end
|
53
54
|
|
54
55
|
##
|
@@ -63,7 +64,7 @@ class LLM::Gemini
|
|
63
64
|
# @param [Hash] params Other parameters (see Gemini docs)
|
64
65
|
# @raise (see LLM::Provider#request)
|
65
66
|
# @note (see LLM::Gemini::Images#create)
|
66
|
-
# @return [LLM::Response
|
67
|
+
# @return [LLM::Response]
|
67
68
|
def edit(image:, prompt:, model: "gemini-2.0-flash-exp-image-generation", **params)
|
68
69
|
req = Net::HTTP::Post.new("/v1beta/models/#{model}:generateContent?key=#{key}", headers)
|
69
70
|
image = LLM.File(image)
|
@@ -73,7 +74,7 @@ class LLM::Gemini
|
|
73
74
|
}.merge!(params)).b
|
74
75
|
set_body_stream(req, StringIO.new(body))
|
75
76
|
res = execute(request: req)
|
76
|
-
LLM::Response
|
77
|
+
LLM::Response.new(res).extend(LLM::Gemini::Response::Image)
|
77
78
|
end
|
78
79
|
|
79
80
|
##
|
@@ -93,7 +94,7 @@ class LLM::Gemini
|
|
93
94
|
@provider.instance_variable_get(:@key)
|
94
95
|
end
|
95
96
|
|
96
|
-
def
|
97
|
+
def system_prompt
|
97
98
|
<<~PROMPT
|
98
99
|
Your task is to generate one or more image(s) from
|
99
100
|
text I will provide to you. Your response *MUST* include
|
@@ -102,7 +103,7 @@ class LLM::Gemini
|
|
102
103
|
PROMPT
|
103
104
|
end
|
104
105
|
|
105
|
-
[:
|
106
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
106
107
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
107
108
|
end
|
108
109
|
end
|
@@ -38,18 +38,12 @@ class LLM::Gemini
|
|
38
38
|
# @see https://ai.google.dev/api/models?hl=en#method:-models.list Gemini docs
|
39
39
|
# @param [Hash] params Other parameters (see Gemini docs)
|
40
40
|
# @raise (see LLM::Provider#request)
|
41
|
-
# @return [LLM::Response
|
41
|
+
# @return [LLM::Response]
|
42
42
|
def all(**params)
|
43
43
|
query = URI.encode_www_form(params.merge!(key: key))
|
44
44
|
req = Net::HTTP::Get.new("/v1beta/models?#{query}", headers)
|
45
45
|
res = execute(request: req)
|
46
|
-
LLM::Response
|
47
|
-
models = modellist.body["models"].map do |model|
|
48
|
-
model = model.transform_keys { snakecase(_1) }
|
49
|
-
LLM::Model.from_hash(model).tap { _1.provider = @provider }
|
50
|
-
end
|
51
|
-
modellist.models = models
|
52
|
-
}
|
46
|
+
LLM::Response.new(res)
|
53
47
|
end
|
54
48
|
|
55
49
|
private
|
data/lib/llm/providers/gemini/{response_parser/completion_parser.rb → response/completion.rb}
RENAMED
@@ -1,30 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module LLM::Gemini::
|
4
|
-
|
5
|
-
def
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
def
|
10
|
-
{
|
11
|
-
model:,
|
12
|
-
prompt_tokens:,
|
13
|
-
completion_tokens:,
|
14
|
-
total_tokens:,
|
15
|
-
choices: format_choices(response)
|
16
|
-
}
|
17
|
-
end
|
3
|
+
module LLM::Gemini::Response
|
4
|
+
module Completion
|
5
|
+
def model = body.modelVersion
|
6
|
+
def prompt_tokens = body.usageMetadata.promptTokenCount
|
7
|
+
def completion_tokens = body.usageMetadata.candidatesTokenCount
|
8
|
+
def total_tokens = body.usageMetadata.totalTokenCount
|
9
|
+
def choices = format_choices
|
18
10
|
|
19
11
|
private
|
20
12
|
|
21
|
-
def format_choices
|
13
|
+
def format_choices
|
22
14
|
candidates.map.with_index do |choice, index|
|
15
|
+
choice = LLM::Object.from_hash(choice)
|
23
16
|
content = choice.content
|
24
17
|
role, parts = content.role, content.parts
|
25
18
|
text = parts.filter_map { _1["text"] }.join
|
26
19
|
tools = parts.filter_map { _1["functionCall"] }
|
27
|
-
extra = {index:, response
|
20
|
+
extra = {index:, response: self, tool_calls: format_tool_calls(tools), original_tool_calls: tools}
|
28
21
|
LLM::Message.new(role, text, extra)
|
29
22
|
end
|
30
23
|
end
|
@@ -35,12 +28,5 @@ module LLM::Gemini::ResponseParser
|
|
35
28
|
LLM::Object.new(function)
|
36
29
|
end
|
37
30
|
end
|
38
|
-
|
39
|
-
def body = @body
|
40
|
-
def model = body.modelVersion
|
41
|
-
def prompt_tokens = body.usageMetadata.promptTokenCount
|
42
|
-
def completion_tokens = body.usageMetadata.candidatesTokenCount
|
43
|
-
def total_tokens = body.usageMetadata.totalTokenCount
|
44
|
-
def candidates = body.candidates
|
45
31
|
end
|
46
32
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM::Gemini::Response
|
4
|
+
module File
|
5
|
+
def name = respond_to?(:file) ? file.name : body.name
|
6
|
+
def display_name = respond_to?(:file) ? file.displayName : body.displayName
|
7
|
+
def mime_type = respond_to?(:file) ? file.mimeType : body.mimeType
|
8
|
+
def uri = respond_to?(:file) ? file.uri : body.uri
|
9
|
+
def file? = true
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM::Gemini::Response
|
4
|
+
module Image
|
5
|
+
##
|
6
|
+
# @return [Array<StringIO>]
|
7
|
+
def images
|
8
|
+
candidates.flat_map do |candidate|
|
9
|
+
parts = candidate["content"]["parts"]
|
10
|
+
parts.filter_map do
|
11
|
+
data = _1.dig(:inlineData, :data)
|
12
|
+
next unless data
|
13
|
+
StringIO.new(data.unpack1("m0"))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Returns one or more image URLs, or an empty array
|
20
|
+
# @note
|
21
|
+
# Gemini's image generation API does not return URLs, so this method
|
22
|
+
# will always return an empty array.
|
23
|
+
# @return [Array<String>]
|
24
|
+
def urls = []
|
25
|
+
end
|
26
|
+
end
|
File without changes
|
data/lib/llm/providers/gemini.rb
CHANGED
@@ -29,10 +29,11 @@ module LLM
|
|
29
29
|
# bot.chat ["Describe the image", LLM::File("/images/capybara.png")]
|
30
30
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
31
31
|
class Gemini < Provider
|
32
|
+
require_relative "gemini/response/embedding"
|
33
|
+
require_relative "gemini/response/completion"
|
32
34
|
require_relative "gemini/error_handler"
|
33
35
|
require_relative "gemini/format"
|
34
36
|
require_relative "gemini/stream_parser"
|
35
|
-
require_relative "gemini/response_parser"
|
36
37
|
require_relative "gemini/models"
|
37
38
|
require_relative "gemini/images"
|
38
39
|
require_relative "gemini/files"
|
@@ -61,7 +62,7 @@ module LLM
|
|
61
62
|
req = Net::HTTP::Post.new(path, headers)
|
62
63
|
req.body = JSON.dump({content: {parts: [{text: input}]}})
|
63
64
|
res = execute(request: req)
|
64
|
-
Response
|
65
|
+
LLM::Response.new(res).extend(LLM::Gemini::Response::Embedding)
|
65
66
|
end
|
66
67
|
|
67
68
|
##
|
@@ -71,7 +72,7 @@ module LLM
|
|
71
72
|
# @param params (see LLM::Provider#complete)
|
72
73
|
# @example (see LLM::Provider#complete)
|
73
74
|
# @raise (see LLM::Provider#request)
|
74
|
-
# @raise [LLM::
|
75
|
+
# @raise [LLM::PromptError]
|
75
76
|
# When given an object a provider does not understand
|
76
77
|
# @return (see LLM::Provider#complete)
|
77
78
|
def complete(prompt, params = {})
|
@@ -86,7 +87,7 @@ module LLM
|
|
86
87
|
body = JSON.dump({contents: format(messages)}.merge!(params))
|
87
88
|
set_body_stream(req, StringIO.new(body))
|
88
89
|
res = execute(request: req, stream:)
|
89
|
-
Response
|
90
|
+
LLM::Response.new(res).extend(LLM::Gemini::Response::Completion)
|
90
91
|
end
|
91
92
|
|
92
93
|
##
|
@@ -140,10 +141,6 @@ module LLM
|
|
140
141
|
)
|
141
142
|
end
|
142
143
|
|
143
|
-
def response_parser
|
144
|
-
LLM::Gemini::ResponseParser
|
145
|
-
end
|
146
|
-
|
147
144
|
def stream_parser
|
148
145
|
LLM::Gemini::StreamParser
|
149
146
|
end
|
@@ -46,6 +46,12 @@ module LLM
|
|
46
46
|
raise NotImplementedError
|
47
47
|
end
|
48
48
|
|
49
|
+
##
|
50
|
+
# @raise [NotImplementedError]
|
51
|
+
def vector_stores
|
52
|
+
raise NotImplementedError
|
53
|
+
end
|
54
|
+
|
49
55
|
##
|
50
56
|
# Returns the default model for chat completions
|
51
57
|
# @see https://ollama.com/library/qwen3 qwen3
|
File without changes
|
@@ -28,13 +28,16 @@ module LLM::Ollama::Format
|
|
28
28
|
|
29
29
|
def format_content(content)
|
30
30
|
case content
|
31
|
+
when File
|
32
|
+
content.close unless content.closed?
|
33
|
+
format_content(LLM.File(content.path))
|
31
34
|
when LLM::File
|
32
35
|
if content.image?
|
33
36
|
{content: "This message has an image associated with it", images: [content.to_b64]}
|
34
37
|
else
|
35
|
-
raise LLM::
|
36
|
-
|
37
|
-
|
38
|
+
raise LLM::PromptError, "The given object (an instance of #{content.class}) " \
|
39
|
+
"is not an image, and therefore not supported by the " \
|
40
|
+
"Ollama API"
|
38
41
|
end
|
39
42
|
when String
|
40
43
|
{content:}
|
@@ -43,8 +46,8 @@ module LLM::Ollama::Format
|
|
43
46
|
when LLM::Function::Return
|
44
47
|
throw(:abort, {role: "tool", tool_call_id: content.id, content: JSON.dump(content.value)})
|
45
48
|
else
|
46
|
-
raise LLM::
|
47
|
-
|
49
|
+
raise LLM::PromptError, "The given object (an instance of #{content.class}) " \
|
50
|
+
"is not supported by the Ollama API"
|
48
51
|
end
|
49
52
|
end
|
50
53
|
|
File without changes
|
@@ -39,18 +39,12 @@ class LLM::Ollama
|
|
39
39
|
# @see https://ollama.com/library Ollama library
|
40
40
|
# @param [Hash] params Other parameters (see Ollama docs)
|
41
41
|
# @raise (see LLM::Provider#request)
|
42
|
-
# @return [LLM::Response
|
42
|
+
# @return [LLM::Response]
|
43
43
|
def all(**params)
|
44
44
|
query = URI.encode_www_form(params)
|
45
45
|
req = Net::HTTP::Get.new("/api/tags?#{query}", headers)
|
46
46
|
res = execute(request: req)
|
47
|
-
LLM::Response
|
48
|
-
models = modellist.body["models"].map do |model|
|
49
|
-
model = model.transform_keys { snakecase(_1) }
|
50
|
-
LLM::Model.from_hash(model).tap { _1.provider = @provider }
|
51
|
-
end
|
52
|
-
modellist.models = models
|
53
|
-
}
|
47
|
+
LLM::Response.new(res)
|
54
48
|
end
|
55
49
|
|
56
50
|
private
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM::Ollama::Response
|
4
|
+
module Completion
|
5
|
+
def model = body.model
|
6
|
+
def prompt_tokens = body.prompt_eval_count || 0
|
7
|
+
def completion_tokens = body.eval_count || 0
|
8
|
+
def total_tokens = prompt_tokens + completion_tokens
|
9
|
+
def message = body.message
|
10
|
+
def choices = [format_choices]
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def format_choices
|
15
|
+
role, content, calls = message.to_h.values_at("role", "content", "tool_calls")
|
16
|
+
extra = {response: self, tool_calls: format_tool_calls(calls)}
|
17
|
+
LLM::Message.new(role, content, extra)
|
18
|
+
end
|
19
|
+
|
20
|
+
def format_tool_calls(tools)
|
21
|
+
return [] unless tools
|
22
|
+
tools.filter_map do |tool|
|
23
|
+
next unless tool["function"]
|
24
|
+
LLM::Object.new(tool["function"])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module LLM::Ollama::Response
|
5
|
+
module Embedding
|
6
|
+
def embeddings = data.map { _1["embedding"] }
|
7
|
+
def prompt_tokens = body.dig("usage", "prompt_tokens") || 0
|
8
|
+
def total_tokens = body.dig("usage", "total_tokens") || 0
|
9
|
+
end
|
10
|
+
end
|