llm.rb 1.0.1 → 2.0.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 +120 -179
- data/lib/llm/bot.rb +105 -78
- data/lib/llm/buffer.rb +18 -84
- data/lib/llm/builder.rb +61 -0
- data/lib/llm/function.rb +5 -1
- data/lib/llm/providers/anthropic/format/completion_format.rb +39 -23
- data/lib/llm/providers/deepseek/format/completion_format.rb +13 -2
- data/lib/llm/providers/gemini/audio.rb +2 -2
- data/lib/llm/providers/gemini/format/completion_format.rb +26 -15
- data/lib/llm/providers/gemini/images.rb +1 -1
- data/lib/llm/providers/gemini/stream_parser.rb +46 -25
- data/lib/llm/providers/ollama/format/completion_format.rb +33 -14
- data/lib/llm/providers/openai/format/completion_format.rb +47 -27
- data/lib/llm/providers/openai/format/respond_format.rb +22 -7
- data/lib/llm/schema/object.rb +23 -2
- data/lib/llm/tool/param.rb +75 -0
- data/lib/llm/tool.rb +5 -2
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +1 -0
- metadata +3 -5
- data/lib/llm/bot/builder.rb +0 -31
- data/lib/llm/bot/conversable.rb +0 -37
- data/lib/llm/bot/prompt/completion.rb +0 -49
- data/lib/llm/bot/prompt/respond.rb +0 -49
data/lib/llm/bot.rb
CHANGED
|
@@ -13,25 +13,18 @@ module LLM
|
|
|
13
13
|
#
|
|
14
14
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
15
15
|
# bot = LLM::Bot.new(llm)
|
|
16
|
-
# url = "https://
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
16
|
+
# url = "https://upload.wikimedia.org/wikipedia/commons/c/c7/Lisc_lipy.jpg"
|
|
17
|
+
#
|
|
18
|
+
# prompt = bot.build_prompt do
|
|
19
|
+
# it.system "Your task is to answer all user queries"
|
|
20
|
+
# it.user ["Tell me about this URL", bot.image_url(url)]
|
|
21
|
+
# it.user ["Tell me about this PDF", bot.local_file("handbook.pdf")]
|
|
22
22
|
# end
|
|
23
|
+
# bot.chat(prompt)
|
|
23
24
|
#
|
|
24
|
-
# #
|
|
25
|
-
#
|
|
25
|
+
# # The full conversation history is in bot.messages
|
|
26
|
+
# bot.messages.each { print "[#{_1.role}] ", _1.content, "\n" }
|
|
26
27
|
class Bot
|
|
27
|
-
require_relative "bot/prompt/completion"
|
|
28
|
-
require_relative "bot/prompt/respond"
|
|
29
|
-
require_relative "bot/conversable"
|
|
30
|
-
require_relative "bot/builder"
|
|
31
|
-
|
|
32
|
-
include Conversable
|
|
33
|
-
include Builder
|
|
34
|
-
|
|
35
28
|
##
|
|
36
29
|
# Returns an Enumerable for the messages in a conversation
|
|
37
30
|
# @return [LLM::Buffer<LLM::Message>]
|
|
@@ -45,7 +38,6 @@ module LLM
|
|
|
45
38
|
# Any parameter the provider supports can be included and
|
|
46
39
|
# not only those listed here.
|
|
47
40
|
# @option params [String] :model Defaults to the provider's default model
|
|
48
|
-
# @option params [#to_json, nil] :schema Defaults to nil
|
|
49
41
|
# @option params [Array<LLM::Function>, nil] :tools Defaults to nil
|
|
50
42
|
def initialize(provider, params = {})
|
|
51
43
|
@provider = provider
|
|
@@ -54,56 +46,51 @@ module LLM
|
|
|
54
46
|
end
|
|
55
47
|
|
|
56
48
|
##
|
|
57
|
-
# Maintain a conversation via the chat completions API
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
# @
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
params = {role: :user}.merge!(params)
|
|
78
|
-
tap { async_completion(prompt, params) }
|
|
79
|
-
end
|
|
49
|
+
# Maintain a conversation via the chat completions API.
|
|
50
|
+
# This method immediately sends a request to the LLM and returns the response.
|
|
51
|
+
#
|
|
52
|
+
# @param prompt (see LLM::Provider#complete)
|
|
53
|
+
# @param params The params, including optional :role (defaults to :user), :stream, :tools, :schema etc.
|
|
54
|
+
# @return [LLM::Response] Returns the LLM's response for this turn.
|
|
55
|
+
# @example
|
|
56
|
+
# llm = LLM.openai(key: ENV["KEY"])
|
|
57
|
+
# bot = LLM::Bot.new(llm)
|
|
58
|
+
# response = bot.chat("Hello, what is your name?")
|
|
59
|
+
# puts response.choices[0].content
|
|
60
|
+
def chat(prompt, params = {})
|
|
61
|
+
prompt, params, messages = fetch(prompt, params)
|
|
62
|
+
params = params.merge(messages: [*@messages.to_a, *messages])
|
|
63
|
+
params = @params.merge(params)
|
|
64
|
+
res = @provider.complete(prompt, params)
|
|
65
|
+
@messages.concat [LLM::Message.new(params[:role] || :user, prompt)]
|
|
66
|
+
@messages.concat messages
|
|
67
|
+
@messages.concat [res.choices[-1]]
|
|
68
|
+
res
|
|
80
69
|
end
|
|
81
70
|
|
|
82
71
|
##
|
|
83
|
-
# Maintain a conversation via the responses API
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
# @
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
tap { async_response(prompt, params) }
|
|
106
|
-
end
|
|
72
|
+
# Maintain a conversation via the responses API.
|
|
73
|
+
# This method immediately sends a request to the LLM and returns the response.
|
|
74
|
+
#
|
|
75
|
+
# @note Not all LLM providers support this API
|
|
76
|
+
# @param prompt (see LLM::Provider#complete)
|
|
77
|
+
# @param params The params, including optional :role (defaults to :user), :stream, :tools, :schema etc.
|
|
78
|
+
# @return [LLM::Response] Returns the LLM's response for this turn.
|
|
79
|
+
# @example
|
|
80
|
+
# llm = LLM.openai(key: ENV["KEY"])
|
|
81
|
+
# bot = LLM::Bot.new(llm)
|
|
82
|
+
# res = bot.respond("What is the capital of France?")
|
|
83
|
+
# puts res.output_text
|
|
84
|
+
def respond(prompt, params = {})
|
|
85
|
+
prompt, params, messages = fetch(prompt, params)
|
|
86
|
+
res_id = @messages.find(&:assistant?)&.response&.response_id
|
|
87
|
+
params = params.merge(previous_response_id: res_id, input: messages).compact
|
|
88
|
+
params = @params.merge(params)
|
|
89
|
+
res = @provider.responses.create(prompt, params)
|
|
90
|
+
@messages.concat [LLM::Message.new(params[:role] || :user, prompt)]
|
|
91
|
+
@messages.concat messages
|
|
92
|
+
@messages.concat [res.choices[-1]]
|
|
93
|
+
res
|
|
107
94
|
end
|
|
108
95
|
|
|
109
96
|
##
|
|
@@ -118,24 +105,12 @@ module LLM
|
|
|
118
105
|
# Returns an array of functions that can be called
|
|
119
106
|
# @return [Array<LLM::Function>]
|
|
120
107
|
def functions
|
|
121
|
-
messages
|
|
108
|
+
@messages
|
|
122
109
|
.select(&:assistant?)
|
|
123
110
|
.flat_map(&:functions)
|
|
124
111
|
.select(&:pending?)
|
|
125
112
|
end
|
|
126
113
|
|
|
127
|
-
##
|
|
128
|
-
# @example
|
|
129
|
-
# llm = LLM.openai(key: ENV["KEY"])
|
|
130
|
-
# bot = LLM::Bot.new(llm, stream: $stdout)
|
|
131
|
-
# bot.chat("Hello", role: :user).flush
|
|
132
|
-
# Drains the buffer and returns all messages as an array
|
|
133
|
-
# @return [Array<LLM::Message>]
|
|
134
|
-
def drain
|
|
135
|
-
messages.drain
|
|
136
|
-
end
|
|
137
|
-
alias_method :flush, :drain
|
|
138
|
-
|
|
139
114
|
##
|
|
140
115
|
# Returns token usage for the conversation
|
|
141
116
|
# @note
|
|
@@ -144,7 +119,59 @@ module LLM
|
|
|
144
119
|
# if there are no assistant messages
|
|
145
120
|
# @return [LLM::Object]
|
|
146
121
|
def usage
|
|
147
|
-
messages.find(&:assistant?)&.usage || LLM::Object.from_hash({})
|
|
122
|
+
@messages.find(&:assistant?)&.usage || LLM::Object.from_hash({})
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
##
|
|
126
|
+
# Build a prompt
|
|
127
|
+
# @example
|
|
128
|
+
# prompt = bot.build_prompt do
|
|
129
|
+
# it.system "Your task is to assist the user"
|
|
130
|
+
# it.user "Hello, can you assist me?"
|
|
131
|
+
# end
|
|
132
|
+
# bot.chat(prompt)
|
|
133
|
+
def build_prompt(&)
|
|
134
|
+
LLM::Builder.new(&).tap(&:call)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# Recongize an object as a URL to an image
|
|
139
|
+
# @param [String] url
|
|
140
|
+
# The URL
|
|
141
|
+
# @return [LLM::Object]
|
|
142
|
+
# Returns a tagged object
|
|
143
|
+
def image_url(url)
|
|
144
|
+
LLM::Object.from_hash(value: url, kind: :image_url)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
##
|
|
148
|
+
# Recongize an object as a local file
|
|
149
|
+
# @param [String] path
|
|
150
|
+
# The path
|
|
151
|
+
# @return [LLM::Object]
|
|
152
|
+
# Returns a tagged object
|
|
153
|
+
def local_file(path)
|
|
154
|
+
LLM::Object.from_hash(value: LLM.File(path), kind: :local_file)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
##
|
|
158
|
+
# Reconginize an object as a remote file
|
|
159
|
+
# @param [LLM::Response] res
|
|
160
|
+
# The response
|
|
161
|
+
# @return [LLM::Object]
|
|
162
|
+
# Returns a tagged object
|
|
163
|
+
def remote_file(res)
|
|
164
|
+
LLM::Object.from_hash(value: res, kind: :remote_file)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def fetch(prompt, params)
|
|
170
|
+
return [prompt, params, []] unless LLM::Builder === prompt
|
|
171
|
+
messages = prompt.to_a
|
|
172
|
+
prompt = messages.shift
|
|
173
|
+
params.merge!(role: prompt.role)
|
|
174
|
+
[prompt.content, params, messages]
|
|
148
175
|
end
|
|
149
176
|
end
|
|
150
177
|
end
|
data/lib/llm/buffer.rb
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
module LLM
|
|
4
4
|
##
|
|
5
5
|
# {LLM::Buffer LLM::Buffer} provides an Enumerable object that
|
|
6
|
-
#
|
|
7
|
-
# a request to the LLM when a response is needed.
|
|
6
|
+
# tracks messages in a conversation thread.
|
|
8
7
|
class Buffer
|
|
9
8
|
include Enumerable
|
|
10
9
|
|
|
@@ -13,19 +12,24 @@ module LLM
|
|
|
13
12
|
# @return [LLM::Buffer]
|
|
14
13
|
def initialize(provider)
|
|
15
14
|
@provider = provider
|
|
16
|
-
@
|
|
17
|
-
|
|
15
|
+
@messages = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# Append an array
|
|
20
|
+
# @param [Array<LLM::Message>] ary
|
|
21
|
+
# The array to append
|
|
22
|
+
def concat(ary)
|
|
23
|
+
@messages.concat(ary)
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
##
|
|
21
27
|
# @yield [LLM::Message]
|
|
22
28
|
# Yields each message in the conversation thread
|
|
23
|
-
# @raise (see LLM::Provider#complete)
|
|
24
29
|
# @return [void]
|
|
25
30
|
def each(...)
|
|
26
31
|
if block_given?
|
|
27
|
-
|
|
28
|
-
@completed.each { yield(_1) }
|
|
32
|
+
@messages.each { yield(_1) }
|
|
29
33
|
else
|
|
30
34
|
enum_for(:each, ...)
|
|
31
35
|
end
|
|
@@ -53,19 +57,15 @@ module LLM
|
|
|
53
57
|
# The number of messages to return
|
|
54
58
|
# @return [LLM::Message, Array<LLM::Message>, nil]
|
|
55
59
|
def last(n = nil)
|
|
56
|
-
|
|
57
|
-
n.nil? ? @completed.last : @completed.last(n)
|
|
58
|
-
else
|
|
59
|
-
n.nil? ? to_a.last : to_a.last(n)
|
|
60
|
-
end
|
|
60
|
+
n.nil? ? @messages.last : @messages.last(n)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
##
|
|
64
|
-
# @param [[LLM::Message
|
|
65
|
-
# A message
|
|
64
|
+
# @param [[LLM::Message]] item
|
|
65
|
+
# A message to add to the buffer
|
|
66
66
|
# @return [void]
|
|
67
67
|
def <<(item)
|
|
68
|
-
@
|
|
68
|
+
@messages << item
|
|
69
69
|
self
|
|
70
70
|
end
|
|
71
71
|
alias_method :push, :<<
|
|
@@ -76,87 +76,21 @@ module LLM
|
|
|
76
76
|
# @return [LLM::Message, nil]
|
|
77
77
|
# Returns a message, or nil
|
|
78
78
|
def [](index)
|
|
79
|
-
|
|
80
|
-
if Range === index
|
|
81
|
-
slice = @completed[index]
|
|
82
|
-
(slice.nil? || slice.size < index.size) ? to_a[index] : slice
|
|
83
|
-
else
|
|
84
|
-
@completed[index]
|
|
85
|
-
end
|
|
86
|
-
else
|
|
87
|
-
to_a[index]
|
|
88
|
-
end
|
|
79
|
+
@messages[index]
|
|
89
80
|
end
|
|
90
81
|
|
|
91
82
|
##
|
|
92
83
|
# @return [String]
|
|
93
84
|
def inspect
|
|
94
85
|
"#<#{self.class.name}:0x#{object_id.to_s(16)} " \
|
|
95
|
-
"
|
|
86
|
+
"message_count=#{@messages.size}>"
|
|
96
87
|
end
|
|
97
88
|
|
|
98
89
|
##
|
|
99
90
|
# Returns true when the buffer is empty
|
|
100
91
|
# @return [Boolean]
|
|
101
92
|
def empty?
|
|
102
|
-
@
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
##
|
|
106
|
-
# @example
|
|
107
|
-
# llm = LLM.openai(key: ENV["KEY"])
|
|
108
|
-
# bot = LLM::Bot.new(llm, stream: $stdout)
|
|
109
|
-
# bot.chat "Hello", role: :user
|
|
110
|
-
# bot.messages.flush
|
|
111
|
-
# @see LLM::Bot#drain
|
|
112
|
-
# @note
|
|
113
|
-
# This method is especially useful when using the streaming API.
|
|
114
|
-
# Drains the buffer and returns all messages as an array
|
|
115
|
-
# @return [Array<LLM::Message>]
|
|
116
|
-
def drain
|
|
117
|
-
to_a
|
|
118
|
-
end
|
|
119
|
-
alias_method :flush, :drain
|
|
120
|
-
|
|
121
|
-
private
|
|
122
|
-
|
|
123
|
-
def empty!
|
|
124
|
-
message, params, method = @pending.pop
|
|
125
|
-
if method == :complete
|
|
126
|
-
complete!(message, params)
|
|
127
|
-
elsif method == :respond
|
|
128
|
-
respond!(message, params)
|
|
129
|
-
else
|
|
130
|
-
raise LLM::Error, "Unknown method: #{method}"
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def complete!(message, params)
|
|
135
|
-
oldparams = @pending.map { _1[1] }
|
|
136
|
-
pendings = @pending.map { _1[0] }
|
|
137
|
-
messages = [*@completed, *pendings]
|
|
138
|
-
role = message.role
|
|
139
|
-
completion = @provider.complete(
|
|
140
|
-
message.content,
|
|
141
|
-
[*oldparams, params.merge(role:, messages:)].inject({}, &:merge!)
|
|
142
|
-
)
|
|
143
|
-
@completed.concat([*pendings, message, *completion.choices[0]])
|
|
144
|
-
@pending.clear
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def respond!(message, params)
|
|
148
|
-
oldparams = @pending.map { _1[1] }
|
|
149
|
-
pendings = @pending.map { _1[0] }
|
|
150
|
-
messages = [*pendings]
|
|
151
|
-
role = message.role
|
|
152
|
-
params = [
|
|
153
|
-
*oldparams,
|
|
154
|
-
params.merge(input: messages),
|
|
155
|
-
@response ? {previous_response_id: @response.response_id} : {}
|
|
156
|
-
].inject({}, &:merge!)
|
|
157
|
-
@response = @provider.responses.create(message.content, params.merge(role:))
|
|
158
|
-
@completed.concat([*pendings, message, *@response.choices[0]])
|
|
159
|
-
@pending.clear
|
|
93
|
+
@messages.empty?
|
|
160
94
|
end
|
|
161
95
|
end
|
|
162
96
|
end
|
data/lib/llm/builder.rb
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# The {LLM::Builder LLM::Builder} class can build a collection
|
|
5
|
+
# of messages that can be sent in a single request.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# llm = LLM.openai(key: ENV["KEY"])
|
|
9
|
+
# bot = LLM::Bot.new(llm)
|
|
10
|
+
# prompt = bot.build_prompt do
|
|
11
|
+
# it.system "Your task is to assist the user"
|
|
12
|
+
# it.user "Hello. Can you assist me?"
|
|
13
|
+
# end
|
|
14
|
+
# res = bot.chat(prompt)
|
|
15
|
+
class LLM::Builder
|
|
16
|
+
##
|
|
17
|
+
# @param [Proc] evaluator
|
|
18
|
+
# The evaluator
|
|
19
|
+
def initialize(&evaluator)
|
|
20
|
+
@buffer = []
|
|
21
|
+
@evaluator = evaluator
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# @return [void]
|
|
26
|
+
def call
|
|
27
|
+
@evaluator.call(self)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# @param [String] content
|
|
32
|
+
# The message
|
|
33
|
+
# @param [Symbol] role
|
|
34
|
+
# The role (eg user, system)
|
|
35
|
+
# @return [void]
|
|
36
|
+
def chat(content, role: :user)
|
|
37
|
+
@buffer << LLM::Message.new(role, content)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# @param [String] content
|
|
42
|
+
# The message content
|
|
43
|
+
# @return [void]
|
|
44
|
+
def user(content)
|
|
45
|
+
chat(content, role: :user)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# @param [String] content
|
|
50
|
+
# The message content
|
|
51
|
+
# @return [void]
|
|
52
|
+
def system(content)
|
|
53
|
+
chat(content, role: :system)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# @return [Array]
|
|
58
|
+
def to_a
|
|
59
|
+
@buffer.dup
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/llm/function.rb
CHANGED
|
@@ -45,29 +45,12 @@ module LLM::Anthropic::Format
|
|
|
45
45
|
content.empty? ? throw(:abort, nil) : [content]
|
|
46
46
|
when Array
|
|
47
47
|
content.empty? ? throw(:abort, nil) : content.flat_map { format_content(_1) }
|
|
48
|
-
when
|
|
49
|
-
|
|
50
|
-
when File
|
|
51
|
-
content.close unless content.closed?
|
|
52
|
-
format_content(LLM.File(content.path))
|
|
53
|
-
when LLM::File
|
|
54
|
-
if content.image?
|
|
55
|
-
[{type: :image, source: {type: "base64", media_type: content.mime_type, data: content.to_b64}}]
|
|
56
|
-
elsif content.pdf?
|
|
57
|
-
[{type: :document, source: {type: "base64", media_type: content.mime_type, data: content.to_b64}}]
|
|
58
|
-
else
|
|
59
|
-
raise LLM::PromptError, "The given object (an instance of #{content.class}) " \
|
|
60
|
-
"is not an image or PDF, and therefore not supported by the " \
|
|
61
|
-
"Anthropic API"
|
|
62
|
-
end
|
|
63
|
-
when LLM::Response
|
|
64
|
-
if content.file?
|
|
65
|
-
[{type: content.file_type, source: {type: :file, file_id: content.id}}]
|
|
66
|
-
else
|
|
67
|
-
prompt_error!(content)
|
|
68
|
-
end
|
|
48
|
+
when LLM::Object
|
|
49
|
+
format_object(content)
|
|
69
50
|
when String
|
|
70
51
|
[{type: :text, text: content}]
|
|
52
|
+
when LLM::Response
|
|
53
|
+
format_remote_file(content)
|
|
71
54
|
when LLM::Message
|
|
72
55
|
format_content(content.content)
|
|
73
56
|
when LLM::Function::Return
|
|
@@ -77,9 +60,42 @@ module LLM::Anthropic::Format
|
|
|
77
60
|
end
|
|
78
61
|
end
|
|
79
62
|
|
|
63
|
+
def format_object(object)
|
|
64
|
+
case object.kind
|
|
65
|
+
when :image_url
|
|
66
|
+
[{type: :image, source: {type: "url", url: object.value.to_s}}]
|
|
67
|
+
when :local_file
|
|
68
|
+
format_local_file(object.value)
|
|
69
|
+
when :remote_file
|
|
70
|
+
format_remote_file(object.value)
|
|
71
|
+
else
|
|
72
|
+
prompt_error!(content)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def format_local_file(file)
|
|
77
|
+
if file.image?
|
|
78
|
+
[{type: :image, source: {type: "base64", media_type: file.mime_type, data: file.to_b64}}]
|
|
79
|
+
elsif file.pdf?
|
|
80
|
+
[{type: :document, source: {type: "base64", media_type: file.mime_type, data: file.to_b64}}]
|
|
81
|
+
else
|
|
82
|
+
raise LLM::PromptError, "The given object (an instance of #{file.class}) " \
|
|
83
|
+
"is not an image or PDF, and therefore not supported by the " \
|
|
84
|
+
"Anthropic API"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def format_remote_file(file)
|
|
89
|
+
prompt_error!(file) unless file.file?
|
|
90
|
+
[{type: file.file_type, source: {type: :file, file_id: file.id}}]
|
|
91
|
+
end
|
|
92
|
+
|
|
80
93
|
def prompt_error!(content)
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
if LLM::Object === content
|
|
95
|
+
else
|
|
96
|
+
raise LLM::PromptError, "The given object (an instance of #{content.class}) " \
|
|
97
|
+
"is not supported by the Anthropic API."
|
|
98
|
+
end
|
|
83
99
|
end
|
|
84
100
|
|
|
85
101
|
def message = @message
|
|
@@ -36,9 +36,10 @@ module LLM::DeepSeek::Format
|
|
|
36
36
|
format_content(content.content)
|
|
37
37
|
when LLM::Function::Return
|
|
38
38
|
throw(:abort, {role: "tool", tool_call_id: content.id, content: JSON.dump(content.value)})
|
|
39
|
+
when LLM::Object
|
|
40
|
+
prompt_error!(content)
|
|
39
41
|
else
|
|
40
|
-
|
|
41
|
-
"is not supported by the DeepSeek chat completions API"
|
|
42
|
+
prompt_error!(content)
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
|
|
@@ -61,6 +62,16 @@ module LLM::DeepSeek::Format
|
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
def prompt_error!(object)
|
|
66
|
+
if LLM::Object === object
|
|
67
|
+
raise LLM::PromptError, "The given LLM::Object with kind '#{content.kind}' is not " \
|
|
68
|
+
"supported by the DeepSeek API"
|
|
69
|
+
else
|
|
70
|
+
raise LLM::PromptError, "The given object (an instance of #{object.class}) " \
|
|
71
|
+
"is not supported by the DeepSeek API"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
64
75
|
def message = @message
|
|
65
76
|
def content = message.content
|
|
66
77
|
def returns = content.grep(LLM::Function::Return)
|
|
@@ -43,7 +43,7 @@ class LLM::Gemini
|
|
|
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
|
-
LLM.File(file)
|
|
46
|
+
LLM::Object.from_hash(value: LLM.File(file), kind: :local_file)
|
|
47
47
|
], params.merge(role: :user, model:)
|
|
48
48
|
res.tap { _1.define_singleton_method(:text) { choices[0].content } }
|
|
49
49
|
end
|
|
@@ -65,7 +65,7 @@ class LLM::Gemini
|
|
|
65
65
|
res = @provider.complete [
|
|
66
66
|
"Your task is to translate the contents of an audio file into English",
|
|
67
67
|
"Your response should include the translation, and nothing else",
|
|
68
|
-
LLM.File(file)
|
|
68
|
+
LLM::Object.from_hash(value: LLM.File(file), kind: :local_file)
|
|
69
69
|
], params.merge(role: :user, model:)
|
|
70
70
|
res.tap { _1.define_singleton_method(:text) { choices[0].content } }
|
|
71
71
|
end
|
|
@@ -30,37 +30,48 @@ 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
|
-
format_response(content)
|
|
35
|
-
when File
|
|
36
|
-
content.close unless content.closed?
|
|
37
|
-
format_content(LLM.File(content.path))
|
|
38
|
-
when LLM::File
|
|
39
|
-
file = content
|
|
40
|
-
[{inline_data: {mime_type: file.mime_type, data: file.to_b64}}]
|
|
41
33
|
when String
|
|
42
34
|
[{text: content}]
|
|
35
|
+
when LLM::Response
|
|
36
|
+
format_remote_file(content)
|
|
43
37
|
when LLM::Message
|
|
44
38
|
format_content(content.content)
|
|
45
39
|
when LLM::Function::Return
|
|
46
40
|
[{functionResponse: {name: content.name, response: content.value}}]
|
|
41
|
+
when LLM::Object
|
|
42
|
+
format_object(content)
|
|
47
43
|
else
|
|
48
44
|
prompt_error!(content)
|
|
49
45
|
end
|
|
50
46
|
end
|
|
51
47
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
[{file_data: {mime_type:
|
|
48
|
+
def format_object(object)
|
|
49
|
+
case object.kind
|
|
50
|
+
when :image_url
|
|
51
|
+
[{file_data: {mime_type: "image/*", file_uri: object.value.to_s}}]
|
|
52
|
+
when :local_file
|
|
53
|
+
file = object.value
|
|
54
|
+
[{inline_data: {mime_type: file.mime_type, data: file.to_b64}}]
|
|
55
|
+
when :remote_file
|
|
56
|
+
format_remote_file(object.value)
|
|
56
57
|
else
|
|
57
|
-
prompt_error!(
|
|
58
|
+
prompt_error!(object)
|
|
58
59
|
end
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
def format_remote_file(file)
|
|
63
|
+
return prompt_error!(file) unless file.file?
|
|
64
|
+
[{file_data: {mime_type: file.mime_type, file_uri: file.uri}}]
|
|
65
|
+
end
|
|
66
|
+
|
|
61
67
|
def prompt_error!(object)
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
if LLM::Object === object
|
|
69
|
+
raise LLM::PromptError, "The given LLM::Object with kind '#{content.kind}' is not " \
|
|
70
|
+
"supported by the Gemini API"
|
|
71
|
+
else
|
|
72
|
+
raise LLM::PromptError, "The given object (an instance of #{object.class}) " \
|
|
73
|
+
"is not supported by the Gemini API"
|
|
74
|
+
end
|
|
64
75
|
end
|
|
65
76
|
|
|
66
77
|
def message = @message
|
|
@@ -69,7 +69,7 @@ class LLM::Gemini
|
|
|
69
69
|
# @return [LLM::Response]
|
|
70
70
|
def edit(image:, prompt:, model: "gemini-2.5-flash-image-preview", **params)
|
|
71
71
|
req = Net::HTTP::Post.new("/v1beta/models/#{model}:generateContent?key=#{key}", headers)
|
|
72
|
-
image = LLM.File(image)
|
|
72
|
+
image = LLM::Object.from_hash(value: LLM.File(image), kind: :local_file)
|
|
73
73
|
body = JSON.dump({
|
|
74
74
|
contents: [{parts: [{text: edit_prompt}, {text: prompt}, format.format_content(image)]}],
|
|
75
75
|
generationConfig: {responseModalities: ["TEXT", "IMAGE"]}
|