llm.rb 4.1.0 → 4.3.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 +2 -2
- data/README.md +241 -166
- data/lib/llm/agent.rb +65 -37
- data/lib/llm/bot.rb +118 -30
- data/lib/llm/buffer.rb +6 -0
- data/lib/llm/function/tracing.rb +19 -0
- data/lib/llm/function.rb +28 -3
- data/lib/llm/json_adapter.rb +12 -12
- data/lib/llm/message.rb +19 -4
- data/lib/llm/prompt.rb +85 -0
- data/lib/llm/provider.rb +62 -10
- data/lib/llm/providers/anthropic/error_handler.rb +27 -5
- data/lib/llm/providers/anthropic/files.rb +22 -16
- data/lib/llm/providers/anthropic/models.rb +4 -3
- data/lib/llm/providers/anthropic.rb +6 -5
- data/lib/llm/providers/deepseek.rb +3 -3
- data/lib/llm/providers/gemini/error_handler.rb +34 -12
- data/lib/llm/providers/gemini/files.rb +19 -14
- data/lib/llm/providers/gemini/images.rb +4 -3
- data/lib/llm/providers/gemini/models.rb +4 -3
- data/lib/llm/providers/gemini.rb +9 -7
- data/lib/llm/providers/llamacpp.rb +3 -3
- data/lib/llm/providers/ollama/error_handler.rb +28 -6
- data/lib/llm/providers/ollama/models.rb +4 -3
- data/lib/llm/providers/ollama.rb +9 -7
- data/lib/llm/providers/openai/audio.rb +10 -7
- data/lib/llm/providers/openai/error_handler.rb +41 -14
- data/lib/llm/providers/openai/files.rb +19 -14
- data/lib/llm/providers/openai/images.rb +10 -7
- data/lib/llm/providers/openai/models.rb +4 -3
- data/lib/llm/providers/openai/moderations.rb +4 -3
- data/lib/llm/providers/openai/responses.rb +10 -7
- data/lib/llm/providers/openai/vector_stores.rb +34 -23
- data/lib/llm/providers/openai.rb +9 -7
- data/lib/llm/providers/xai.rb +3 -3
- data/lib/llm/providers/zai.rb +2 -2
- data/lib/llm/schema/object.rb +4 -4
- data/lib/llm/schema.rb +16 -2
- data/lib/llm/server_tool.rb +3 -3
- data/lib/llm/session/deserializer.rb +36 -0
- data/lib/llm/session.rb +3 -0
- data/lib/llm/tracer/logger.rb +192 -0
- data/lib/llm/tracer/null.rb +49 -0
- data/lib/llm/tracer/telemetry.rb +255 -0
- data/lib/llm/tracer.rb +134 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +4 -3
- data/llm.gemspec +6 -3
- metadata +41 -5
- data/lib/llm/builder.rb +0 -79
data/lib/llm/prompt.rb
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# {LLM::Prompt LLM::Prompt} is a small object for composing
|
|
5
|
+
# a single request from multiple role-aware messages.
|
|
6
|
+
# A prompt is not just a string. It is an ordered chain of
|
|
7
|
+
# messages with explicit roles (for example `system` and `user`).
|
|
8
|
+
# Use {LLM::Session#prompt} when building a prompt inside a session.
|
|
9
|
+
# Use `LLM::Prompt.new(provider)` directly when you want to construct
|
|
10
|
+
# or pass prompt objects around explicitly.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# llm = LLM.openai(key: ENV["KEY"])
|
|
14
|
+
# ses = LLM::Session.new(llm)
|
|
15
|
+
#
|
|
16
|
+
# prompt = ses.prompt do
|
|
17
|
+
# system "Your task is to assist the user"
|
|
18
|
+
# user "Hello. Can you assist me?"
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# res = ses.talk(prompt)
|
|
22
|
+
class LLM::Prompt
|
|
23
|
+
##
|
|
24
|
+
# @param [LLM::Provider] provider
|
|
25
|
+
# A provider used to resolve provider-specific role names.
|
|
26
|
+
# @param [Proc] b
|
|
27
|
+
# A block that composes messages. If the block takes one argument,
|
|
28
|
+
# it receives the prompt object. Otherwise the block runs in the
|
|
29
|
+
# prompt context via `instance_eval`.
|
|
30
|
+
def initialize(provider, &b)
|
|
31
|
+
@provider = provider
|
|
32
|
+
@buffer = []
|
|
33
|
+
unless b.nil?
|
|
34
|
+
(b.arity == 1) ? b.call(self) : instance_eval(&b)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# @param [String] content
|
|
40
|
+
# The message
|
|
41
|
+
# @param [Symbol] role
|
|
42
|
+
# The role (eg user, system)
|
|
43
|
+
# @return [void]
|
|
44
|
+
def talk(content, role: @provider.user_role)
|
|
45
|
+
role = case role.to_sym
|
|
46
|
+
when :system then @provider.system_role
|
|
47
|
+
when :user then @provider.user_role
|
|
48
|
+
when :developer then @provider.developer_role
|
|
49
|
+
else role
|
|
50
|
+
end
|
|
51
|
+
@buffer << LLM::Message.new(role, content)
|
|
52
|
+
end
|
|
53
|
+
alias_method :chat, :talk
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# @param [String] content
|
|
57
|
+
# The message content
|
|
58
|
+
# @return [void]
|
|
59
|
+
def user(content)
|
|
60
|
+
chat(content, role: @provider.user_role)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# @param [String] content
|
|
65
|
+
# The message content
|
|
66
|
+
# @return [void]
|
|
67
|
+
def system(content)
|
|
68
|
+
chat(content, role: @provider.system_role)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# @param [String] content
|
|
73
|
+
# The message content
|
|
74
|
+
# @return [void]
|
|
75
|
+
def developer(content)
|
|
76
|
+
chat(content, role: @provider.developer_role)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# @return [Array<LLM::Message>]
|
|
81
|
+
# Returns the prompt messages in order.
|
|
82
|
+
def to_a
|
|
83
|
+
@buffer.dup
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/llm/provider.rb
CHANGED
|
@@ -37,6 +37,7 @@ class LLM::Provider
|
|
|
37
37
|
@timeout = timeout
|
|
38
38
|
@ssl = ssl
|
|
39
39
|
@client = persistent ? persistent_client : transient_client
|
|
40
|
+
@tracer = LLM::Tracer::Null.new(self)
|
|
40
41
|
@base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
|
|
41
42
|
end
|
|
42
43
|
|
|
@@ -45,7 +46,7 @@ class LLM::Provider
|
|
|
45
46
|
# @return [String]
|
|
46
47
|
# @note The secret key is redacted in inspect for security reasons
|
|
47
48
|
def inspect
|
|
48
|
-
"#<#{self.class.name}:0x#{object_id.to_s(16)} @key=[REDACTED] @client=#{@client.inspect}>"
|
|
49
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} @key=[REDACTED] @client=#{@client.inspect} @tracer=#{@tracer.inspect}>"
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
##
|
|
@@ -69,7 +70,7 @@ class LLM::Provider
|
|
|
69
70
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
70
71
|
# messages = [{role: "system", content: "Your task is to answer all of my questions"}]
|
|
71
72
|
# res = llm.complete("5 + 2 ?", messages:)
|
|
72
|
-
# print "[#{res.
|
|
73
|
+
# print "[#{res.messages[0].role}]", res.messages[0].content, "\n"
|
|
73
74
|
# @param [String] prompt
|
|
74
75
|
# The input prompt to be completed
|
|
75
76
|
# @param [Hash] params
|
|
@@ -91,10 +92,10 @@ class LLM::Provider
|
|
|
91
92
|
# Starts a new chat powered by the chat completions API
|
|
92
93
|
# @param prompt (see LLM::Provider#complete)
|
|
93
94
|
# @param params (see LLM::Provider#complete)
|
|
94
|
-
# @return [LLM::
|
|
95
|
+
# @return [LLM::Session]
|
|
95
96
|
def chat(prompt, params = {})
|
|
96
97
|
role = params.delete(:role)
|
|
97
|
-
LLM::
|
|
98
|
+
LLM::Session.new(self, params).talk(prompt, role:)
|
|
98
99
|
end
|
|
99
100
|
|
|
100
101
|
##
|
|
@@ -102,10 +103,10 @@ class LLM::Provider
|
|
|
102
103
|
# @param prompt (see LLM::Provider#complete)
|
|
103
104
|
# @param params (see LLM::Provider#complete)
|
|
104
105
|
# @raise (see LLM::Provider#complete)
|
|
105
|
-
# @return [LLM::
|
|
106
|
+
# @return [LLM::Session]
|
|
106
107
|
def respond(prompt, params = {})
|
|
107
108
|
role = params.delete(:role)
|
|
108
|
-
LLM::
|
|
109
|
+
LLM::Session.new(self, params).respond(prompt, role:)
|
|
109
110
|
end
|
|
110
111
|
|
|
111
112
|
##
|
|
@@ -252,6 +253,36 @@ class LLM::Provider
|
|
|
252
253
|
:developer
|
|
253
254
|
end
|
|
254
255
|
|
|
256
|
+
##
|
|
257
|
+
# @return [Symbol]
|
|
258
|
+
def tool_role
|
|
259
|
+
:tool
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
##
|
|
263
|
+
# @return [LLM::Tracer]
|
|
264
|
+
# Returns an LLM tracer
|
|
265
|
+
def tracer
|
|
266
|
+
@tracer
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
##
|
|
270
|
+
# Set the tracer
|
|
271
|
+
# @example
|
|
272
|
+
# llm = LLM.openai(key: ENV["KEY"])
|
|
273
|
+
# llm.tracer = LLM::Tracer::Logger.new(llm, path: "/path/to/log.txt")
|
|
274
|
+
# # ...
|
|
275
|
+
# @param [LLM::Tracer] tracer
|
|
276
|
+
# A tracer
|
|
277
|
+
# @return [void]
|
|
278
|
+
def tracer=(tracer)
|
|
279
|
+
@tracer = if tracer.nil?
|
|
280
|
+
LLM::Tracer::Null.new(self)
|
|
281
|
+
else
|
|
282
|
+
tracer
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
255
286
|
private
|
|
256
287
|
|
|
257
288
|
attr_reader :client, :base_uri, :host, :port, :timeout, :ssl
|
|
@@ -303,7 +334,8 @@ class LLM::Provider
|
|
|
303
334
|
# @raise [SystemCallError]
|
|
304
335
|
# When there is a network error at the operating system level
|
|
305
336
|
# @return [Net::HTTPResponse]
|
|
306
|
-
def execute(request:, stream: nil, stream_parser: self.stream_parser, &b)
|
|
337
|
+
def execute(request:, operation:, stream: nil, stream_parser: self.stream_parser, model: nil, &b)
|
|
338
|
+
span = @tracer.on_request_start(operation:, model:)
|
|
307
339
|
args = (Net::HTTP === client) ? [request] : [URI.join(base_uri, request.path), request]
|
|
308
340
|
res = if stream
|
|
309
341
|
client.request(*args) do |res|
|
|
@@ -323,18 +355,20 @@ class LLM::Provider
|
|
|
323
355
|
b ? client.request(*args) { (Net::HTTPSuccess === _1) ? b.call(_1) : _1 } :
|
|
324
356
|
client.request(*args)
|
|
325
357
|
end
|
|
326
|
-
handle_response(res)
|
|
358
|
+
[handle_response(res, span), span]
|
|
327
359
|
end
|
|
328
360
|
|
|
329
361
|
##
|
|
330
362
|
# Handles the response from a request
|
|
331
363
|
# @param [Net::HTTPResponse] res
|
|
332
364
|
# The response to handle
|
|
365
|
+
# @param [Object, nil] span
|
|
366
|
+
# The span
|
|
333
367
|
# @return [Net::HTTPResponse]
|
|
334
|
-
def handle_response(res)
|
|
368
|
+
def handle_response(res, span)
|
|
335
369
|
case res
|
|
336
370
|
when Net::HTTPOK then res.body = parse_response(res)
|
|
337
|
-
else error_handler.new(res).raise_error!
|
|
371
|
+
else error_handler.new(@tracer, span, res).raise_error!
|
|
338
372
|
end
|
|
339
373
|
res
|
|
340
374
|
end
|
|
@@ -375,4 +409,22 @@ class LLM::Provider
|
|
|
375
409
|
end
|
|
376
410
|
end
|
|
377
411
|
end
|
|
412
|
+
|
|
413
|
+
##
|
|
414
|
+
# @return [Hash<Symbol, LLM::Tracer>]
|
|
415
|
+
def tracers
|
|
416
|
+
self.class.tracers
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
##
|
|
420
|
+
# Finalizes tracing after a response has been adapted/wrapped.
|
|
421
|
+
# @param [String] operation
|
|
422
|
+
# @param [String, nil] model
|
|
423
|
+
# @param [LLM::Response] res
|
|
424
|
+
# @param [Object, nil] span
|
|
425
|
+
# @return [LLM::Response]
|
|
426
|
+
def finish_trace(operation:, res:, model: nil, span: nil)
|
|
427
|
+
@tracer.on_request_finish(operation:, model:, res:, span:)
|
|
428
|
+
res
|
|
429
|
+
end
|
|
378
430
|
end
|
|
@@ -10,10 +10,21 @@ class LLM::Anthropic
|
|
|
10
10
|
attr_reader :res
|
|
11
11
|
|
|
12
12
|
##
|
|
13
|
+
# @return [Object, nil]
|
|
14
|
+
# The span
|
|
15
|
+
attr_reader :span
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# @param [LLM::Tracer] tracer
|
|
19
|
+
# The tracer
|
|
20
|
+
# @param [Object, nil] span
|
|
21
|
+
# The span
|
|
13
22
|
# @param [Net::HTTPResponse] res
|
|
14
23
|
# The response from the server
|
|
15
24
|
# @return [LLM::Anthropic::ErrorHandler]
|
|
16
|
-
def initialize(res)
|
|
25
|
+
def initialize(tracer, span, res)
|
|
26
|
+
@tracer = tracer
|
|
27
|
+
@span = span
|
|
17
28
|
@res = res
|
|
18
29
|
end
|
|
19
30
|
|
|
@@ -21,15 +32,26 @@ class LLM::Anthropic
|
|
|
21
32
|
# @raise [LLM::Error]
|
|
22
33
|
# Raises a subclass of {LLM::Error LLM::Error}
|
|
23
34
|
def raise_error!
|
|
35
|
+
ex = error
|
|
36
|
+
@tracer.on_request_error(ex:, span:)
|
|
37
|
+
ensure
|
|
38
|
+
raise(ex)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# @return [LLM::Error]
|
|
45
|
+
def error
|
|
24
46
|
case res
|
|
25
47
|
when Net::HTTPServerError
|
|
26
|
-
|
|
48
|
+
LLM::ServerError.new("Server error").tap { _1.response = res }
|
|
27
49
|
when Net::HTTPUnauthorized
|
|
28
|
-
|
|
50
|
+
LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
|
|
29
51
|
when Net::HTTPTooManyRequests
|
|
30
|
-
|
|
52
|
+
LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
|
|
31
53
|
else
|
|
32
|
-
|
|
54
|
+
LLM::Error.new("Unexpected response").tap { _1.response = res }
|
|
33
55
|
end
|
|
34
56
|
end
|
|
35
57
|
end
|
|
@@ -10,10 +10,10 @@ class LLM::Anthropic
|
|
|
10
10
|
# require "llm"
|
|
11
11
|
#
|
|
12
12
|
# llm = LLM.anthropic(key: ENV["KEY"])
|
|
13
|
-
#
|
|
13
|
+
# ses = LLM::Session.new(llm)
|
|
14
14
|
# file = llm.files.create file: "/books/goodread.pdf"
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# ses.talk ["Tell me about this PDF", file]
|
|
16
|
+
# ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
17
17
|
class Files
|
|
18
18
|
##
|
|
19
19
|
# Returns a new Files object
|
|
@@ -38,8 +38,9 @@ class LLM::Anthropic
|
|
|
38
38
|
def all(**params)
|
|
39
39
|
query = URI.encode_www_form(params)
|
|
40
40
|
req = Net::HTTP::Get.new("/v1/files?#{query}", headers)
|
|
41
|
-
res = execute(request: req)
|
|
42
|
-
ResponseAdapter.adapt(res, type: :enumerable)
|
|
41
|
+
res, span = execute(request: req, operation: "request")
|
|
42
|
+
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
43
|
+
finish_trace(operation: "request", res:, span:)
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
##
|
|
@@ -57,8 +58,9 @@ class LLM::Anthropic
|
|
|
57
58
|
req = Net::HTTP::Post.new("/v1/files", headers)
|
|
58
59
|
req["content-type"] = multi.content_type
|
|
59
60
|
set_body_stream(req, multi.body)
|
|
60
|
-
res = execute(request: req)
|
|
61
|
-
ResponseAdapter.adapt(res, type: :file)
|
|
61
|
+
res, span = execute(request: req, operation: "request")
|
|
62
|
+
res = ResponseAdapter.adapt(res, type: :file)
|
|
63
|
+
finish_trace(operation: "request", res:, span:)
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
##
|
|
@@ -76,8 +78,9 @@ class LLM::Anthropic
|
|
|
76
78
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
77
79
|
query = URI.encode_www_form(params)
|
|
78
80
|
req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
|
|
79
|
-
res = execute(request: req)
|
|
80
|
-
ResponseAdapter.adapt(res, type: :file)
|
|
81
|
+
res, span = execute(request: req, operation: "request")
|
|
82
|
+
res = ResponseAdapter.adapt(res, type: :file)
|
|
83
|
+
finish_trace(operation: "request", res:, span:)
|
|
81
84
|
end
|
|
82
85
|
|
|
83
86
|
##
|
|
@@ -95,8 +98,9 @@ class LLM::Anthropic
|
|
|
95
98
|
query = URI.encode_www_form(params)
|
|
96
99
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
97
100
|
req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
|
|
98
|
-
res = execute(request: req)
|
|
99
|
-
ResponseAdapter.adapt(res, type: :file)
|
|
101
|
+
res, span = execute(request: req, operation: "request")
|
|
102
|
+
res = ResponseAdapter.adapt(res, type: :file)
|
|
103
|
+
finish_trace(operation: "request", res:, span:)
|
|
100
104
|
end
|
|
101
105
|
alias_method :retrieve_metadata, :get_metadata
|
|
102
106
|
|
|
@@ -113,8 +117,9 @@ class LLM::Anthropic
|
|
|
113
117
|
def delete(file:)
|
|
114
118
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
115
119
|
req = Net::HTTP::Delete.new("/v1/files/#{file_id}", headers)
|
|
116
|
-
res = execute(request: req)
|
|
117
|
-
LLM::Response.new(res)
|
|
120
|
+
res, span = execute(request: req, operation: "request")
|
|
121
|
+
res = LLM::Response.new(res)
|
|
122
|
+
finish_trace(operation: "request", res:, span:)
|
|
118
123
|
end
|
|
119
124
|
|
|
120
125
|
##
|
|
@@ -137,8 +142,9 @@ class LLM::Anthropic
|
|
|
137
142
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
138
143
|
req = Net::HTTP::Get.new("/v1/files/#{file_id}/content?#{query}", headers)
|
|
139
144
|
io = StringIO.new("".b)
|
|
140
|
-
res = execute(request: req) { |res| res.read_body { |chunk| io << chunk } }
|
|
141
|
-
LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
|
|
145
|
+
res, span = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
|
|
146
|
+
res = LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
|
|
147
|
+
finish_trace(operation: "request", res:, span:)
|
|
142
148
|
end
|
|
143
149
|
|
|
144
150
|
private
|
|
@@ -147,7 +153,7 @@ class LLM::Anthropic
|
|
|
147
153
|
@provider.instance_variable_get(:@key)
|
|
148
154
|
end
|
|
149
155
|
|
|
150
|
-
[:headers, :execute, :set_body_stream].each do |m|
|
|
156
|
+
[:headers, :execute, :set_body_stream, :finish_trace].each do |m|
|
|
151
157
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
152
158
|
end
|
|
153
159
|
end
|
|
@@ -40,13 +40,14 @@ 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 = execute(request: req)
|
|
44
|
-
ResponseAdapter.adapt(res, type: :enumerable)
|
|
43
|
+
res, span = execute(request: req, operation: "request")
|
|
44
|
+
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
45
|
+
finish_trace(operation: "request", res:, span:)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
private
|
|
48
49
|
|
|
49
|
-
[:headers, :execute].each do |m|
|
|
50
|
+
[:headers, :execute, :finish_trace].each do |m|
|
|
50
51
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
51
52
|
end
|
|
52
53
|
end
|
|
@@ -10,9 +10,9 @@ module LLM
|
|
|
10
10
|
# require "llm"
|
|
11
11
|
#
|
|
12
12
|
# llm = LLM.anthropic(key: ENV["KEY"])
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
13
|
+
# ses = LLM::Session.new(llm)
|
|
14
|
+
# ses.talk ["Tell me about this photo", ses.local_file("/images/photo.png")]
|
|
15
|
+
# ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
16
16
|
class Anthropic < Provider
|
|
17
17
|
require_relative "anthropic/error_handler"
|
|
18
18
|
require_relative "anthropic/request_adapter"
|
|
@@ -43,9 +43,10 @@ module LLM
|
|
|
43
43
|
def complete(prompt, params = {})
|
|
44
44
|
params, stream, tools, role = normalize_complete_params(params)
|
|
45
45
|
req = build_complete_request(prompt, params, role)
|
|
46
|
-
res = execute(request: req, stream: stream)
|
|
47
|
-
ResponseAdapter.adapt(res, type: :completion)
|
|
46
|
+
res, span = execute(request: req, stream: stream, operation: "chat", model: params[:model])
|
|
47
|
+
res = ResponseAdapter.adapt(res, type: :completion)
|
|
48
48
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
49
|
+
finish_trace(operation: "chat", model: params[:model], res:, span:)
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
##
|
|
@@ -14,9 +14,9 @@ module LLM
|
|
|
14
14
|
# require "llm"
|
|
15
15
|
#
|
|
16
16
|
# llm = LLM.deepseek(key: ENV["KEY"])
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
17
|
+
# ses = LLM::Session.new(llm)
|
|
18
|
+
# ses.talk ["Tell me about this photo", ses.local_file("/images/photo.png")]
|
|
19
|
+
# ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
20
20
|
class DeepSeek < OpenAI
|
|
21
21
|
require_relative "deepseek/request_adapter"
|
|
22
22
|
include DeepSeek::RequestAdapter
|
|
@@ -10,10 +10,21 @@ class LLM::Gemini
|
|
|
10
10
|
attr_reader :res
|
|
11
11
|
|
|
12
12
|
##
|
|
13
|
+
# @return [Object, nil]
|
|
14
|
+
# The span
|
|
15
|
+
attr_reader :span
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# @param [LLM::Tracer] tracer
|
|
19
|
+
# The tracer
|
|
20
|
+
# @param [Object, nil] span
|
|
21
|
+
# The span
|
|
13
22
|
# @param [Net::HTTPResponse] res
|
|
14
23
|
# The response from the server
|
|
15
24
|
# @return [LLM::Gemini::ErrorHandler]
|
|
16
|
-
def initialize(res)
|
|
25
|
+
def initialize(tracer, span, res)
|
|
26
|
+
@tracer = tracer
|
|
27
|
+
@span = span
|
|
17
28
|
@res = res
|
|
18
29
|
end
|
|
19
30
|
|
|
@@ -21,27 +32,38 @@ class LLM::Gemini
|
|
|
21
32
|
# @raise [LLM::Error]
|
|
22
33
|
# Raises a subclass of {LLM::Error LLM::Error}
|
|
23
34
|
def raise_error!
|
|
35
|
+
ex = error
|
|
36
|
+
@tracer.on_request_error(ex:, span:)
|
|
37
|
+
ensure
|
|
38
|
+
raise(ex)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# @return [LLM::Object]
|
|
45
|
+
def body
|
|
46
|
+
@body ||= LLM.json.load(res.body)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# @return [LLM::Error]
|
|
51
|
+
def error
|
|
24
52
|
case res
|
|
25
53
|
when Net::HTTPServerError
|
|
26
|
-
|
|
54
|
+
LLM::ServerError.new("Server error").tap { _1.response = res }
|
|
27
55
|
when Net::HTTPBadRequest
|
|
28
56
|
reason = body.dig("error", "details", 0, "reason")
|
|
29
57
|
if reason == "API_KEY_INVALID"
|
|
30
|
-
|
|
58
|
+
LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
|
|
31
59
|
else
|
|
32
|
-
|
|
60
|
+
LLM::Error.new("Unexpected response").tap { _1.response = res }
|
|
33
61
|
end
|
|
34
62
|
when Net::HTTPTooManyRequests
|
|
35
|
-
|
|
63
|
+
LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
|
|
36
64
|
else
|
|
37
|
-
|
|
65
|
+
LLM::Error.new("Unexpected response").tap { _1.response = res }
|
|
38
66
|
end
|
|
39
67
|
end
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
def body
|
|
44
|
-
@body ||= LLM.json.load(res.body)
|
|
45
|
-
end
|
|
46
68
|
end
|
|
47
69
|
end
|
|
@@ -18,10 +18,10 @@ class LLM::Gemini
|
|
|
18
18
|
# require "llm"
|
|
19
19
|
#
|
|
20
20
|
# llm = LLM.gemini(key: ENV["KEY"])
|
|
21
|
-
#
|
|
21
|
+
# ses = LLM::Session.new(llm)
|
|
22
22
|
# file = llm.files.create(file: "/audio/haiku.mp3")
|
|
23
|
-
#
|
|
24
|
-
#
|
|
23
|
+
# ses.talk ["Tell me about this file", file]
|
|
24
|
+
# ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
25
25
|
class Files
|
|
26
26
|
##
|
|
27
27
|
# Returns a new Files object
|
|
@@ -46,8 +46,9 @@ class LLM::Gemini
|
|
|
46
46
|
def all(**params)
|
|
47
47
|
query = URI.encode_www_form(params.merge!(key: key))
|
|
48
48
|
req = Net::HTTP::Get.new("/v1beta/files?#{query}", headers)
|
|
49
|
-
res = execute(request: req)
|
|
50
|
-
ResponseAdapter.adapt(res, type: :files)
|
|
49
|
+
res, span = execute(request: req, operation: "request")
|
|
50
|
+
res = ResponseAdapter.adapt(res, type: :files)
|
|
51
|
+
finish_trace(operation: "request", res:, span:)
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
##
|
|
@@ -68,8 +69,9 @@ class LLM::Gemini
|
|
|
68
69
|
req["X-Goog-Upload-Command"] = "upload, finalize"
|
|
69
70
|
file.with_io do |io|
|
|
70
71
|
set_body_stream(req, io)
|
|
71
|
-
res = execute(request: req)
|
|
72
|
-
ResponseAdapter.adapt(res, type: :file)
|
|
72
|
+
res, span = execute(request: req, operation: "request")
|
|
73
|
+
res = ResponseAdapter.adapt(res, type: :file)
|
|
74
|
+
finish_trace(operation: "request", res:, span:)
|
|
73
75
|
end
|
|
74
76
|
end
|
|
75
77
|
|
|
@@ -88,8 +90,9 @@ class LLM::Gemini
|
|
|
88
90
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
|
89
91
|
query = URI.encode_www_form(params.merge!(key: key))
|
|
90
92
|
req = Net::HTTP::Get.new("/v1beta/#{file_id}?#{query}", headers)
|
|
91
|
-
res = execute(request: req)
|
|
92
|
-
ResponseAdapter.adapt(res, type: :file)
|
|
93
|
+
res, span = execute(request: req, operation: "request")
|
|
94
|
+
res = ResponseAdapter.adapt(res, type: :file)
|
|
95
|
+
finish_trace(operation: "request", res:, span:)
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
##
|
|
@@ -106,8 +109,9 @@ class LLM::Gemini
|
|
|
106
109
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
|
107
110
|
query = URI.encode_www_form(params.merge!(key: key))
|
|
108
111
|
req = Net::HTTP::Delete.new("/v1beta/#{file_id}?#{query}", headers)
|
|
109
|
-
res = execute(request: req)
|
|
110
|
-
LLM::Response.new(res)
|
|
112
|
+
res, span = execute(request: req, operation: "request")
|
|
113
|
+
res = LLM::Response.new(res)
|
|
114
|
+
finish_trace(operation: "request", res:, span:)
|
|
111
115
|
end
|
|
112
116
|
|
|
113
117
|
##
|
|
@@ -127,8 +131,9 @@ class LLM::Gemini
|
|
|
127
131
|
req["X-Goog-Upload-Command"] = "start"
|
|
128
132
|
req["X-Goog-Upload-Header-Content-Length"] = file.bytesize
|
|
129
133
|
req["X-Goog-Upload-Header-Content-Type"] = file.mime_type
|
|
130
|
-
req.body = LLM.json.dump(file: {display_name: File.basename(file.path)})
|
|
131
|
-
res = execute(request: req)
|
|
134
|
+
req.body = LLM.json.dump({file: {display_name: File.basename(file.path)}})
|
|
135
|
+
res, span = execute(request: req, operation: "request")
|
|
136
|
+
finish_trace(operation: "request", res: LLM::Response.new(res), span:)
|
|
132
137
|
res["x-goog-upload-url"]
|
|
133
138
|
end
|
|
134
139
|
|
|
@@ -136,7 +141,7 @@ class LLM::Gemini
|
|
|
136
141
|
@provider.instance_variable_get(:@key)
|
|
137
142
|
end
|
|
138
143
|
|
|
139
|
-
[:headers, :execute, :set_body_stream].each do |m|
|
|
144
|
+
[:headers, :execute, :set_body_stream, :finish_trace].each do |m|
|
|
140
145
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
141
146
|
end
|
|
142
147
|
end
|
|
@@ -51,8 +51,9 @@ class LLM::Gemini
|
|
|
51
51
|
instances: [{prompt:}]
|
|
52
52
|
})
|
|
53
53
|
req.body = body
|
|
54
|
-
res = execute(request: req)
|
|
55
|
-
ResponseAdapter.adapt(res, type: :image)
|
|
54
|
+
res, span = execute(request: req, operation: "request")
|
|
55
|
+
res = ResponseAdapter.adapt(res, type: :image)
|
|
56
|
+
finish_trace(operation: "request", model:, res:, span:)
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
##
|
|
@@ -89,7 +90,7 @@ class LLM::Gemini
|
|
|
89
90
|
@provider.instance_variable_get(:@key)
|
|
90
91
|
end
|
|
91
92
|
|
|
92
|
-
[:headers, :execute, :set_body_stream].each do |m|
|
|
93
|
+
[:headers, :execute, :set_body_stream, :finish_trace].each do |m|
|
|
93
94
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
94
95
|
end
|
|
95
96
|
end
|
|
@@ -42,8 +42,9 @@ 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 = execute(request: req)
|
|
46
|
-
ResponseAdapter.adapt(res, type: :models)
|
|
45
|
+
res, span = execute(request: req, operation: "request")
|
|
46
|
+
res = ResponseAdapter.adapt(res, type: :models)
|
|
47
|
+
finish_trace(operation: "request", res:, span:)
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
private
|
|
@@ -52,7 +53,7 @@ class LLM::Gemini
|
|
|
52
53
|
@provider.instance_variable_get(:@key)
|
|
53
54
|
end
|
|
54
55
|
|
|
55
|
-
[:headers, :execute].each do |m|
|
|
56
|
+
[:headers, :execute, :finish_trace].each do |m|
|
|
56
57
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
57
58
|
end
|
|
58
59
|
end
|
data/lib/llm/providers/gemini.rb
CHANGED
|
@@ -14,9 +14,9 @@ module LLM
|
|
|
14
14
|
# require "llm"
|
|
15
15
|
#
|
|
16
16
|
# llm = LLM.gemini(key: ENV["KEY"])
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
17
|
+
# ses = LLM::Session.new(llm)
|
|
18
|
+
# ses.talk ["Tell me about this photo", ses.local_file("/images/photo.png")]
|
|
19
|
+
# ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
20
20
|
class Gemini < Provider
|
|
21
21
|
require_relative "gemini/error_handler"
|
|
22
22
|
require_relative "gemini/request_adapter"
|
|
@@ -49,8 +49,9 @@ module LLM
|
|
|
49
49
|
path = ["/v1beta/models/#{model}", "embedContent?key=#{@key}"].join(":")
|
|
50
50
|
req = Net::HTTP::Post.new(path, headers)
|
|
51
51
|
req.body = LLM.json.dump({content: {parts: [{text: input}]}})
|
|
52
|
-
res = execute(request: req)
|
|
53
|
-
ResponseAdapter.adapt(res, type: :embedding)
|
|
52
|
+
res, span = execute(request: req, operation: "embeddings", model:)
|
|
53
|
+
res = ResponseAdapter.adapt(res, type: :embedding)
|
|
54
|
+
finish_trace(operation: "embeddings", model:, res:, span:)
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
##
|
|
@@ -66,9 +67,10 @@ module LLM
|
|
|
66
67
|
def complete(prompt, params = {})
|
|
67
68
|
params, stream, tools, role, model = normalize_complete_params(params)
|
|
68
69
|
req = build_complete_request(prompt, params, role, model, stream)
|
|
69
|
-
res = execute(request: req, stream: stream)
|
|
70
|
-
ResponseAdapter.adapt(res, type: :completion)
|
|
70
|
+
res, span = execute(request: req, stream: stream, operation: "chat", model:)
|
|
71
|
+
res = ResponseAdapter.adapt(res, type: :completion)
|
|
71
72
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
73
|
+
finish_trace(operation: "chat", model:, res:, span:)
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
##
|