llm.rb 0.7.2 → 0.9.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 +93 -63
- data/lib/llm/{chat → bot}/builder.rb +1 -1
- data/lib/llm/bot/conversable.rb +31 -0
- data/lib/llm/{chat → bot}/prompt/completion.rb +14 -4
- data/lib/llm/{chat → bot}/prompt/respond.rb +16 -5
- data/lib/llm/{chat.rb → bot.rb} +48 -66
- data/lib/llm/buffer.rb +2 -2
- data/lib/llm/error.rb +24 -16
- data/lib/llm/event_handler.rb +44 -0
- data/lib/llm/eventstream/event.rb +69 -0
- data/lib/llm/eventstream/parser.rb +88 -0
- data/lib/llm/eventstream.rb +8 -0
- data/lib/llm/function.rb +9 -12
- data/lib/{json → llm/json}/schema/array.rb +1 -1
- data/lib/llm/message.rb +1 -1
- data/lib/llm/model.rb +1 -1
- data/lib/llm/object/builder.rb +38 -0
- data/lib/llm/object/kernel.rb +45 -0
- data/lib/llm/object.rb +77 -0
- data/lib/llm/provider.rb +68 -26
- data/lib/llm/providers/anthropic/error_handler.rb +3 -3
- data/lib/llm/providers/anthropic/models.rb +3 -7
- data/lib/llm/providers/anthropic/response_parser/completion_parser.rb +5 -5
- data/lib/llm/providers/anthropic/response_parser.rb +1 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +66 -0
- data/lib/llm/providers/anthropic.rb +9 -4
- data/lib/llm/providers/deepseek/format/completion_format.rb +68 -0
- data/lib/llm/providers/deepseek/format.rb +28 -0
- data/lib/llm/providers/deepseek.rb +60 -0
- data/lib/llm/providers/gemini/error_handler.rb +4 -4
- data/lib/llm/providers/gemini/files.rb +13 -16
- data/lib/llm/providers/gemini/images.rb +4 -8
- data/lib/llm/providers/gemini/models.rb +3 -7
- data/lib/llm/providers/gemini/response_parser/completion_parser.rb +2 -2
- data/lib/llm/providers/gemini/stream_parser.rb +69 -0
- data/lib/llm/providers/gemini.rb +19 -11
- data/lib/llm/providers/llamacpp.rb +16 -2
- data/lib/llm/providers/ollama/error_handler.rb +3 -3
- data/lib/llm/providers/ollama/format/completion_format.rb +1 -1
- data/lib/llm/providers/ollama/models.rb +3 -7
- data/lib/llm/providers/ollama/response_parser/completion_parser.rb +2 -2
- data/lib/llm/providers/ollama/stream_parser.rb +44 -0
- data/lib/llm/providers/ollama.rb +16 -9
- data/lib/llm/providers/openai/audio.rb +5 -9
- data/lib/llm/providers/openai/error_handler.rb +3 -3
- data/lib/llm/providers/openai/files.rb +15 -18
- data/lib/llm/providers/openai/format/moderation_format.rb +35 -0
- data/lib/llm/providers/openai/format.rb +3 -3
- data/lib/llm/providers/openai/images.rb +8 -11
- data/lib/llm/providers/openai/models.rb +3 -7
- data/lib/llm/providers/openai/moderations.rb +67 -0
- data/lib/llm/providers/openai/response_parser/completion_parser.rb +5 -5
- data/lib/llm/providers/openai/response_parser/respond_parser.rb +2 -2
- data/lib/llm/providers/openai/response_parser.rb +15 -0
- data/lib/llm/providers/openai/responses.rb +14 -16
- data/lib/llm/providers/openai/stream_parser.rb +77 -0
- data/lib/llm/providers/openai.rb +22 -7
- data/lib/llm/providers/voyageai/error_handler.rb +3 -3
- data/lib/llm/providers/voyageai.rb +1 -1
- data/lib/llm/response/filelist.rb +1 -1
- data/lib/llm/response/image.rb +1 -1
- data/lib/llm/response/modellist.rb +1 -1
- data/lib/llm/response/moderationlist/moderation.rb +47 -0
- data/lib/llm/response/moderationlist.rb +51 -0
- data/lib/llm/response.rb +1 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +13 -4
- data/llm.gemspec +2 -2
- metadata +42 -28
- data/lib/llm/chat/conversable.rb +0 -53
- data/lib/llm/core_ext/ostruct.rb +0 -43
- /data/lib/{json → llm/json}/schema/boolean.rb +0 -0
- /data/lib/{json → llm/json}/schema/integer.rb +0 -0
- /data/lib/{json → llm/json}/schema/leaf.rb +0 -0
- /data/lib/{json → llm/json}/schema/null.rb +0 -0
- /data/lib/{json → llm/json}/schema/number.rb +0 -0
- /data/lib/{json → llm/json}/schema/object.rb +0 -0
- /data/lib/{json → llm/json}/schema/string.rb +0 -0
- /data/lib/{json → llm/json}/schema/version.rb +0 -0
- /data/lib/{json → llm/json}/schema.rb +0 -0
@@ -40,7 +40,7 @@ class LLM::Anthropic
|
|
40
40
|
def all(**params)
|
41
41
|
query = URI.encode_www_form(params)
|
42
42
|
req = Net::HTTP::Get.new("/v1/models?#{query}", headers)
|
43
|
-
res = request
|
43
|
+
res = execute(request: req)
|
44
44
|
LLM::Response::ModelList.new(res).tap { |modellist|
|
45
45
|
models = modellist.body["data"].map do |model|
|
46
46
|
LLM::Model.from_hash(model).tap { _1.provider = @provider }
|
@@ -51,12 +51,8 @@ class LLM::Anthropic
|
|
51
51
|
|
52
52
|
private
|
53
53
|
|
54
|
-
|
55
|
-
@provider.
|
56
|
-
end
|
57
|
-
|
58
|
-
[:headers, :request].each do |m|
|
59
|
-
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
54
|
+
[:headers, :execute].each do |m|
|
55
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
60
56
|
end
|
61
57
|
end
|
62
58
|
end
|
@@ -5,7 +5,7 @@ module LLM::Anthropic::ResponseParser
|
|
5
5
|
# @private
|
6
6
|
class CompletionParser
|
7
7
|
def initialize(body)
|
8
|
-
@body =
|
8
|
+
@body = LLM::Object.from_hash(body)
|
9
9
|
end
|
10
10
|
|
11
11
|
def format(response)
|
@@ -34,16 +34,16 @@ module LLM::Anthropic::ResponseParser
|
|
34
34
|
name: tool.name,
|
35
35
|
arguments: tool.input
|
36
36
|
}
|
37
|
-
|
37
|
+
LLM::Object.new(tool)
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
41
|
def body = @body
|
42
42
|
def role = body.role
|
43
43
|
def model = body.model
|
44
|
-
def prompt_tokens = body.usage
|
45
|
-
def completion_tokens = body.usage
|
46
|
-
def total_tokens = body.usage
|
44
|
+
def prompt_tokens = body.usage&.input_tokens
|
45
|
+
def completion_tokens = body.usage&.output_tokens
|
46
|
+
def total_tokens = body.usage&.total_tokens
|
47
47
|
def parts = body.content
|
48
48
|
def texts = parts.select { _1["type"] == "text" }
|
49
49
|
def tools = parts.select { _1["type"] == "tool_use" }
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::Anthropic
|
4
|
+
##
|
5
|
+
# @private
|
6
|
+
class StreamParser
|
7
|
+
##
|
8
|
+
# Returns the fully constructed response body
|
9
|
+
# @return [LLM::Object]
|
10
|
+
attr_reader :body
|
11
|
+
|
12
|
+
##
|
13
|
+
# @param [#<<] io An IO-like object
|
14
|
+
# @return [LLM::Anthropic::StreamParser]
|
15
|
+
def initialize(io)
|
16
|
+
@body = LLM::Object.new(role: "assistant", content: [])
|
17
|
+
@io = io
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# @param [Hash] chunk
|
22
|
+
# @return [LLM::Anthropic::StreamParser]
|
23
|
+
def parse!(chunk)
|
24
|
+
tap { merge!(chunk) }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def merge!(chunk)
|
30
|
+
if chunk["type"] == "message_start"
|
31
|
+
merge_message!(chunk["message"])
|
32
|
+
elsif chunk["type"] == "content_block_start"
|
33
|
+
@body["content"][chunk["index"]] = chunk["content_block"]
|
34
|
+
elsif chunk["type"] == "content_block_delta"
|
35
|
+
if chunk["delta"]["type"] == "text_delta"
|
36
|
+
@body.content[chunk["index"]]["text"] << chunk["delta"]["text"]
|
37
|
+
@io << chunk["delta"]["text"] if @io.respond_to?(:<<)
|
38
|
+
elsif chunk["delta"]["type"] == "input_json_delta"
|
39
|
+
content = @body.content[chunk["index"]]
|
40
|
+
if Hash === content["input"]
|
41
|
+
content["input"] = chunk["delta"]["partial_json"]
|
42
|
+
else
|
43
|
+
content["input"] << chunk["delta"]["partial_json"]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
elsif chunk["type"] == "message_delta"
|
47
|
+
merge_message!(chunk["delta"])
|
48
|
+
elsif chunk["type"] == "content_block_stop"
|
49
|
+
content = @body.content[chunk["index"]]
|
50
|
+
if content["input"]
|
51
|
+
content["input"] = JSON.parse(content["input"])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def merge_message!(message)
|
57
|
+
message.each do |key, value|
|
58
|
+
@body[key] = if value.respond_to?(:each_pair)
|
59
|
+
merge_message!(value)
|
60
|
+
else
|
61
|
+
value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -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/format"
|
8
9
|
require_relative "anthropic/error_handler"
|
10
|
+
require_relative "anthropic/stream_parser"
|
9
11
|
require_relative "anthropic/response_parser"
|
10
|
-
require_relative "anthropic/response_parser/completion_parser"
|
11
|
-
require_relative "anthropic/format"
|
12
12
|
require_relative "anthropic/models"
|
13
13
|
include Format
|
14
14
|
|
@@ -50,12 +50,13 @@ module LLM
|
|
50
50
|
def complete(prompt, params = {})
|
51
51
|
params = {role: :user, model: default_model, max_tokens: 1024}.merge!(params)
|
52
52
|
params = [params, format_tools(params)].inject({}, &:merge!).compact
|
53
|
-
role = params.delete(:role)
|
53
|
+
role, stream = params.delete(:role), params.delete(:stream)
|
54
|
+
params[:stream] = true if stream.respond_to?(:<<) || stream == true
|
54
55
|
req = Net::HTTP::Post.new("/v1/messages", headers)
|
55
56
|
messages = [*(params.delete(:messages) || []), Message.new(role, prompt)]
|
56
57
|
body = JSON.dump({messages: [format(messages)].flatten}.merge!(params))
|
57
58
|
set_body_stream(req, StringIO.new(body))
|
58
|
-
res = request
|
59
|
+
res = execute(request: req, stream:)
|
59
60
|
Response::Completion.new(res).extend(response_parser)
|
60
61
|
end
|
61
62
|
|
@@ -95,6 +96,10 @@ module LLM
|
|
95
96
|
LLM::Anthropic::ResponseParser
|
96
97
|
end
|
97
98
|
|
99
|
+
def stream_parser
|
100
|
+
LLM::Anthropic::StreamParser
|
101
|
+
end
|
102
|
+
|
98
103
|
def error_handler
|
99
104
|
LLM::Anthropic::ErrorHandler
|
100
105
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM::DeepSeek::Format
|
4
|
+
##
|
5
|
+
# @private
|
6
|
+
class CompletionFormat
|
7
|
+
##
|
8
|
+
# @param [LLM::Message, Hash] message
|
9
|
+
# The message to format
|
10
|
+
def initialize(message)
|
11
|
+
@message = message
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Formats the message for the OpenAI chat completions API
|
16
|
+
# @return [Hash]
|
17
|
+
def format
|
18
|
+
catch(:abort) do
|
19
|
+
if Hash === message
|
20
|
+
{role: message[:role], content: format_content(message[:content])}
|
21
|
+
elsif message.tool_call?
|
22
|
+
{role: message.role, content: nil, tool_calls: message.extra[:original_tool_calls]}
|
23
|
+
else
|
24
|
+
format_message
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def format_content(content)
|
32
|
+
case content
|
33
|
+
when String
|
34
|
+
content.to_s
|
35
|
+
when LLM::Message
|
36
|
+
format_content(content.content)
|
37
|
+
when LLM::Function::Return
|
38
|
+
throw(:abort, {role: "tool", tool_call_id: content.id, content: JSON.dump(content.value)})
|
39
|
+
else
|
40
|
+
raise LLM::Error::PromptError, "The given object (an instance of #{content.class}) " \
|
41
|
+
"is not supported by the DeepSeek chat completions API"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def format_message
|
46
|
+
case content
|
47
|
+
when Array
|
48
|
+
format_array
|
49
|
+
else
|
50
|
+
{role: message.role, content: format_content(content)}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def format_array
|
55
|
+
if content.empty?
|
56
|
+
nil
|
57
|
+
elsif returns.any?
|
58
|
+
returns.map { {role: "tool", tool_call_id: _1.id, content: JSON.dump(_1.value)} }
|
59
|
+
else
|
60
|
+
{role: message.role, content: content.flat_map { format_content(_1) }}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def message = @message
|
65
|
+
def content = message.content
|
66
|
+
def returns = content.grep(LLM::Function::Return)
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::DeepSeek
|
4
|
+
##
|
5
|
+
# @private
|
6
|
+
module Format
|
7
|
+
require_relative "format/completion_format"
|
8
|
+
##
|
9
|
+
# @param [Array<LLM::Message>] messages
|
10
|
+
# The messages to format
|
11
|
+
# @return [Array<Hash>]
|
12
|
+
def format(messages, ...)
|
13
|
+
messages.filter_map do |message|
|
14
|
+
CompletionFormat.new(message).format
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
##
|
21
|
+
# @param [Hash] params
|
22
|
+
# @return [Hash]
|
23
|
+
def format_tools(params)
|
24
|
+
tools = params.delete(:tools)
|
25
|
+
(tools.nil? || tools.empty?) ? {} : {tools: tools.map { _1.format(self) }}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "openai" unless defined?(LLM::OpenAI)
|
4
|
+
|
5
|
+
module LLM
|
6
|
+
##
|
7
|
+
# The DeepSeek class implements a provider for
|
8
|
+
# [DeepSeek](https://deepseek.com)
|
9
|
+
# through its OpenAI-compatible API provided via
|
10
|
+
# their [web platform](https://platform.deepseek.com).
|
11
|
+
class DeepSeek < OpenAI
|
12
|
+
require_relative "deepseek/format"
|
13
|
+
include DeepSeek::Format
|
14
|
+
|
15
|
+
##
|
16
|
+
# @param (see LLM::Provider#initialize)
|
17
|
+
# @return [LLM::DeepSeek]
|
18
|
+
def initialize(host: "api.deepseek.com", port: 443, ssl: true, **)
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# @raise [NotImplementedError]
|
24
|
+
def files
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# @raise [NotImplementedError]
|
30
|
+
def images
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# @raise [NotImplementedError]
|
36
|
+
def audio
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# @raise [NotImplementedError]
|
42
|
+
def moderations
|
43
|
+
raise NotImplementedError
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# @raise [NotImplementedError]
|
48
|
+
def responses
|
49
|
+
raise NotImplementedError
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Returns the default model for chat completions
|
54
|
+
# @see https://api-docs.deepseek.com/quick_start/pricing deepseek-chat
|
55
|
+
# @return [String]
|
56
|
+
def default_model
|
57
|
+
"deepseek-chat"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -25,14 +25,14 @@ class LLM::Gemini
|
|
25
25
|
when Net::HTTPBadRequest
|
26
26
|
reason = body.dig("error", "details", 0, "reason")
|
27
27
|
if reason == "API_KEY_INVALID"
|
28
|
-
raise LLM::
|
28
|
+
raise LLM::UnauthorizedError.new { _1.response = res }, "Authentication error"
|
29
29
|
else
|
30
|
-
raise LLM::
|
30
|
+
raise LLM::ResponseError.new { _1.response = res }, "Unexpected response"
|
31
31
|
end
|
32
32
|
when Net::HTTPTooManyRequests
|
33
|
-
raise LLM::
|
33
|
+
raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
|
34
34
|
else
|
35
|
-
raise LLM::
|
35
|
+
raise LLM::ResponseError.new { _1.response = res }, "Unexpected response"
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
@@ -13,23 +13,24 @@ class LLM::Gemini
|
|
13
13
|
# in the prompt over and over again (which could be the case in a
|
14
14
|
# multi-turn conversation).
|
15
15
|
#
|
16
|
-
# @example
|
16
|
+
# @example example #1
|
17
17
|
# #!/usr/bin/env ruby
|
18
18
|
# require "llm"
|
19
19
|
#
|
20
20
|
# llm = LLM.gemini(ENV["KEY"])
|
21
|
-
# bot = LLM::
|
21
|
+
# bot = LLM::Bot.new(llm)
|
22
22
|
# file = llm.files.create file: "/audio/haiku.mp3"
|
23
23
|
# bot.chat(file)
|
24
24
|
# bot.chat("Describe the audio file I sent to you")
|
25
25
|
# bot.chat("The audio file is the first message I sent to you.")
|
26
26
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
27
|
-
#
|
27
|
+
#
|
28
|
+
# @example example #2
|
28
29
|
# #!/usr/bin/env ruby
|
29
30
|
# require "llm"
|
30
31
|
#
|
31
32
|
# llm = LLM.gemini(ENV["KEY"])
|
32
|
-
# bot = LLM::
|
33
|
+
# bot = LLM::Bot.new(llm)
|
33
34
|
# file = llm.files.create file: "/audio/haiku.mp3"
|
34
35
|
# bot.chat(["Describe the audio file I sent to you", file])
|
35
36
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
@@ -57,11 +58,11 @@ class LLM::Gemini
|
|
57
58
|
def all(**params)
|
58
59
|
query = URI.encode_www_form(params.merge!(key: key))
|
59
60
|
req = Net::HTTP::Get.new("/v1beta/files?#{query}", headers)
|
60
|
-
res = request
|
61
|
+
res = execute(request: req)
|
61
62
|
LLM::Response::FileList.new(res).tap { |filelist|
|
62
63
|
files = filelist.body["files"]&.map do |file|
|
63
64
|
file = file.transform_keys { snakecase(_1) }
|
64
|
-
|
65
|
+
LLM::Object.from_hash(file)
|
65
66
|
end || []
|
66
67
|
filelist.files = files
|
67
68
|
}
|
@@ -85,7 +86,7 @@ class LLM::Gemini
|
|
85
86
|
req["X-Goog-Upload-Command"] = "upload, finalize"
|
86
87
|
file.with_io do |io|
|
87
88
|
set_body_stream(req, io)
|
88
|
-
res = request
|
89
|
+
res = execute(request: req)
|
89
90
|
LLM::Response::File.new(res)
|
90
91
|
end
|
91
92
|
end
|
@@ -105,7 +106,7 @@ class LLM::Gemini
|
|
105
106
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
106
107
|
query = URI.encode_www_form(params.merge!(key: key))
|
107
108
|
req = Net::HTTP::Get.new("/v1beta/#{file_id}?#{query}", headers)
|
108
|
-
res = request
|
109
|
+
res = execute(request: req)
|
109
110
|
LLM::Response::File.new(res)
|
110
111
|
end
|
111
112
|
|
@@ -123,7 +124,7 @@ class LLM::Gemini
|
|
123
124
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
124
125
|
query = URI.encode_www_form(params.merge!(key: key))
|
125
126
|
req = Net::HTTP::Delete.new("/v1beta/#{file_id}?#{query}", headers)
|
126
|
-
request
|
127
|
+
execute(request: req)
|
127
128
|
end
|
128
129
|
|
129
130
|
##
|
@@ -144,20 +145,16 @@ class LLM::Gemini
|
|
144
145
|
req["X-Goog-Upload-Header-Content-Length"] = file.bytesize
|
145
146
|
req["X-Goog-Upload-Header-Content-Type"] = file.mime_type
|
146
147
|
req.body = JSON.dump(file: {display_name: File.basename(file.path)})
|
147
|
-
res = request
|
148
|
+
res = execute(request: req)
|
148
149
|
res["x-goog-upload-url"]
|
149
150
|
end
|
150
151
|
|
151
|
-
def http
|
152
|
-
@provider.instance_variable_get(:@http)
|
153
|
-
end
|
154
|
-
|
155
152
|
def key
|
156
153
|
@provider.instance_variable_get(:@key)
|
157
154
|
end
|
158
155
|
|
159
|
-
[:headers, :
|
160
|
-
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
156
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
157
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
161
158
|
end
|
162
159
|
end
|
163
160
|
end
|
@@ -47,7 +47,7 @@ class LLM::Gemini
|
|
47
47
|
generationConfig: {responseModalities: ["TEXT", "IMAGE"]}
|
48
48
|
}.merge!(params))
|
49
49
|
req.body = body
|
50
|
-
res = request
|
50
|
+
res = execute(request: req)
|
51
51
|
LLM::Response::Image.new(res).extend(response_parser)
|
52
52
|
end
|
53
53
|
|
@@ -72,7 +72,7 @@ class LLM::Gemini
|
|
72
72
|
generationConfig: {responseModalities: ["TEXT", "IMAGE"]}
|
73
73
|
}.merge!(params)).b
|
74
74
|
set_body_stream(req, StringIO.new(body))
|
75
|
-
res = request
|
75
|
+
res = execute(request: req)
|
76
76
|
LLM::Response::Image.new(res).extend(response_parser)
|
77
77
|
end
|
78
78
|
|
@@ -93,12 +93,8 @@ class LLM::Gemini
|
|
93
93
|
@provider.instance_variable_get(:@key)
|
94
94
|
end
|
95
95
|
|
96
|
-
|
97
|
-
@provider.
|
98
|
-
end
|
99
|
-
|
100
|
-
[:response_parser, :headers, :request, :set_body_stream].each do |m|
|
101
|
-
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
96
|
+
[:response_parser, :headers, :execute, :set_body_stream].each do |m|
|
97
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
102
98
|
end
|
103
99
|
end
|
104
100
|
end
|
@@ -42,7 +42,7 @@ class LLM::Gemini
|
|
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
|
-
res = request
|
45
|
+
res = execute(request: req)
|
46
46
|
LLM::Response::ModelList.new(res).tap { |modellist|
|
47
47
|
models = modellist.body["models"].map do |model|
|
48
48
|
model = model.transform_keys { snakecase(_1) }
|
@@ -54,16 +54,12 @@ class LLM::Gemini
|
|
54
54
|
|
55
55
|
private
|
56
56
|
|
57
|
-
def http
|
58
|
-
@provider.instance_variable_get(:@http)
|
59
|
-
end
|
60
|
-
|
61
57
|
def key
|
62
58
|
@provider.instance_variable_get(:@key)
|
63
59
|
end
|
64
60
|
|
65
|
-
[:headers, :
|
66
|
-
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
61
|
+
[:headers, :execute].each do |m|
|
62
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
67
63
|
end
|
68
64
|
end
|
69
65
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module LLM::Gemini::ResponseParser
|
4
4
|
class CompletionParser
|
5
5
|
def initialize(body)
|
6
|
-
@body =
|
6
|
+
@body = LLM::Object.from_hash(body)
|
7
7
|
end
|
8
8
|
|
9
9
|
def format(response)
|
@@ -32,7 +32,7 @@ module LLM::Gemini::ResponseParser
|
|
32
32
|
def format_tool_calls(tools)
|
33
33
|
(tools || []).map do |tool|
|
34
34
|
function = {name: tool.name, arguments: tool.args}
|
35
|
-
|
35
|
+
LLM::Object.new(function)
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::Gemini
|
4
|
+
##
|
5
|
+
# @private
|
6
|
+
class StreamParser
|
7
|
+
##
|
8
|
+
# Returns the fully constructed response body
|
9
|
+
# @return [LLM::Object]
|
10
|
+
attr_reader :body
|
11
|
+
|
12
|
+
##
|
13
|
+
# @param [#<<] io An IO-like object
|
14
|
+
# @return [LLM::Gemini::StreamParser]
|
15
|
+
def initialize(io)
|
16
|
+
@body = LLM::Object.new
|
17
|
+
@io = io
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# @param [Hash] chunk
|
22
|
+
# @return [LLM::Gemini::StreamParser]
|
23
|
+
def parse!(chunk)
|
24
|
+
tap { merge!(chunk) }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def merge!(chunk)
|
30
|
+
chunk.each do |key, value|
|
31
|
+
if key == "candidates"
|
32
|
+
@body.candidates ||= []
|
33
|
+
merge_candidates!(value)
|
34
|
+
else
|
35
|
+
@body[key] = value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def merge_candidates!(candidates)
|
41
|
+
candidates.each.with_index do |candidate, i|
|
42
|
+
if @body.candidates[i].nil?
|
43
|
+
merge_one(@body.candidates, candidate, i)
|
44
|
+
else
|
45
|
+
merge_two(@body.candidates, candidate, i)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def merge_one(candidates, candidate, i)
|
51
|
+
candidate
|
52
|
+
.dig("content", "parts")
|
53
|
+
&.filter_map { _1["text"] }
|
54
|
+
&.each { @io << _1 if @io.respond_to?(:<<) }
|
55
|
+
candidates[i] = candidate
|
56
|
+
end
|
57
|
+
|
58
|
+
def merge_two(candidates, candidate, i)
|
59
|
+
parts = candidates[i].dig("content", "parts")
|
60
|
+
parts&.each&.with_index do |part, j|
|
61
|
+
if part["text"]
|
62
|
+
target = candidate["content"]["parts"][j]
|
63
|
+
part["text"] << target["text"]
|
64
|
+
@io << target["text"] if @io.respond_to?(:<<)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/llm/providers/gemini.rb
CHANGED
@@ -10,31 +10,34 @@ module LLM
|
|
10
10
|
# prompt for files under 20MB or via the Gemini Files API for
|
11
11
|
# files that are over 20MB
|
12
12
|
#
|
13
|
-
# @example
|
13
|
+
# @example example #1
|
14
14
|
# #!/usr/bin/env ruby
|
15
15
|
# require "llm"
|
16
16
|
#
|
17
17
|
# llm = LLM.gemini(ENV["KEY"])
|
18
|
-
# bot = LLM::
|
19
|
-
# bot.chat LLM
|
18
|
+
# bot = LLM::Bot.new(llm)
|
19
|
+
# bot.chat LLM.File("/images/capybara.png")
|
20
20
|
# bot.chat "Describe the image"
|
21
21
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
22
|
-
#
|
22
|
+
#
|
23
|
+
# @example example #2
|
23
24
|
# #!/usr/bin/env ruby
|
24
25
|
# require "llm"
|
25
26
|
#
|
26
27
|
# llm = LLM.gemini(ENV["KEY"])
|
27
|
-
# bot = LLM::
|
28
|
+
# bot = LLM::Bot.new(llm)
|
28
29
|
# bot.chat ["Describe the image", LLM::File("/images/capybara.png")]
|
29
30
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
30
31
|
class Gemini < Provider
|
31
32
|
require_relative "gemini/error_handler"
|
32
|
-
require_relative "gemini/response_parser"
|
33
33
|
require_relative "gemini/format"
|
34
|
+
require_relative "gemini/stream_parser"
|
35
|
+
require_relative "gemini/response_parser"
|
36
|
+
require_relative "gemini/models"
|
34
37
|
require_relative "gemini/images"
|
35
38
|
require_relative "gemini/files"
|
36
39
|
require_relative "gemini/audio"
|
37
|
-
|
40
|
+
|
38
41
|
include Format
|
39
42
|
|
40
43
|
HOST = "generativelanguage.googleapis.com"
|
@@ -57,7 +60,7 @@ module LLM
|
|
57
60
|
path = ["/v1beta/models/#{model}", "embedContent?key=#{@key}"].join(":")
|
58
61
|
req = Net::HTTP::Post.new(path, headers)
|
59
62
|
req.body = JSON.dump({content: {parts: [{text: input}]}})
|
60
|
-
res = request
|
63
|
+
res = execute(request: req)
|
61
64
|
Response::Embedding.new(res).extend(response_parser)
|
62
65
|
end
|
63
66
|
|
@@ -74,14 +77,15 @@ module LLM
|
|
74
77
|
def complete(prompt, params = {})
|
75
78
|
params = {role: :user, model: default_model}.merge!(params)
|
76
79
|
params = [params, format_schema(params), format_tools(params)].inject({}, &:merge!).compact
|
77
|
-
role, model = [:role, :model].map { params.delete(_1) }
|
80
|
+
role, model, stream = [:role, :model, :stream].map { params.delete(_1) }
|
81
|
+
action = stream ? "streamGenerateContent?key=#{@key}&alt=sse" : "generateContent?key=#{@key}"
|
78
82
|
model.respond_to?(:id) ? model.id : model
|
79
|
-
path = ["/v1beta/models/#{model}",
|
83
|
+
path = ["/v1beta/models/#{model}", action].join(":")
|
80
84
|
req = Net::HTTP::Post.new(path, headers)
|
81
85
|
messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
82
86
|
body = JSON.dump({contents: format(messages)}.merge!(params))
|
83
87
|
set_body_stream(req, StringIO.new(body))
|
84
|
-
res = request
|
88
|
+
res = execute(request: req, stream:)
|
85
89
|
Response::Completion.new(res).extend(response_parser)
|
86
90
|
end
|
87
91
|
|
@@ -140,6 +144,10 @@ module LLM
|
|
140
144
|
LLM::Gemini::ResponseParser
|
141
145
|
end
|
142
146
|
|
147
|
+
def stream_parser
|
148
|
+
LLM::Gemini::StreamParser
|
149
|
+
end
|
150
|
+
|
143
151
|
def error_handler
|
144
152
|
LLM::Gemini::ErrorHandler
|
145
153
|
end
|