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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +2 -2
  3. data/README.md +241 -166
  4. data/lib/llm/agent.rb +65 -37
  5. data/lib/llm/bot.rb +118 -30
  6. data/lib/llm/buffer.rb +6 -0
  7. data/lib/llm/function/tracing.rb +19 -0
  8. data/lib/llm/function.rb +28 -3
  9. data/lib/llm/json_adapter.rb +12 -12
  10. data/lib/llm/message.rb +19 -4
  11. data/lib/llm/prompt.rb +85 -0
  12. data/lib/llm/provider.rb +62 -10
  13. data/lib/llm/providers/anthropic/error_handler.rb +27 -5
  14. data/lib/llm/providers/anthropic/files.rb +22 -16
  15. data/lib/llm/providers/anthropic/models.rb +4 -3
  16. data/lib/llm/providers/anthropic.rb +6 -5
  17. data/lib/llm/providers/deepseek.rb +3 -3
  18. data/lib/llm/providers/gemini/error_handler.rb +34 -12
  19. data/lib/llm/providers/gemini/files.rb +19 -14
  20. data/lib/llm/providers/gemini/images.rb +4 -3
  21. data/lib/llm/providers/gemini/models.rb +4 -3
  22. data/lib/llm/providers/gemini.rb +9 -7
  23. data/lib/llm/providers/llamacpp.rb +3 -3
  24. data/lib/llm/providers/ollama/error_handler.rb +28 -6
  25. data/lib/llm/providers/ollama/models.rb +4 -3
  26. data/lib/llm/providers/ollama.rb +9 -7
  27. data/lib/llm/providers/openai/audio.rb +10 -7
  28. data/lib/llm/providers/openai/error_handler.rb +41 -14
  29. data/lib/llm/providers/openai/files.rb +19 -14
  30. data/lib/llm/providers/openai/images.rb +10 -7
  31. data/lib/llm/providers/openai/models.rb +4 -3
  32. data/lib/llm/providers/openai/moderations.rb +4 -3
  33. data/lib/llm/providers/openai/responses.rb +10 -7
  34. data/lib/llm/providers/openai/vector_stores.rb +34 -23
  35. data/lib/llm/providers/openai.rb +9 -7
  36. data/lib/llm/providers/xai.rb +3 -3
  37. data/lib/llm/providers/zai.rb +2 -2
  38. data/lib/llm/schema/object.rb +4 -4
  39. data/lib/llm/schema.rb +16 -2
  40. data/lib/llm/server_tool.rb +3 -3
  41. data/lib/llm/session/deserializer.rb +36 -0
  42. data/lib/llm/session.rb +3 -0
  43. data/lib/llm/tracer/logger.rb +192 -0
  44. data/lib/llm/tracer/null.rb +49 -0
  45. data/lib/llm/tracer/telemetry.rb +255 -0
  46. data/lib/llm/tracer.rb +134 -0
  47. data/lib/llm/version.rb +1 -1
  48. data/lib/llm.rb +4 -3
  49. data/llm.gemspec +6 -3
  50. metadata +41 -5
  51. 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.choices[0].role}]", res.choices[0].content, "\n"
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::Bot]
95
+ # @return [LLM::Session]
95
96
  def chat(prompt, params = {})
96
97
  role = params.delete(:role)
97
- LLM::Bot.new(self, params).chat(prompt, role:)
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::Bot]
106
+ # @return [LLM::Session]
106
107
  def respond(prompt, params = {})
107
108
  role = params.delete(:role)
108
- LLM::Bot.new(self, params).respond(prompt, role:)
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
- raise LLM::ServerError.new { _1.response = res }, "Server error"
48
+ LLM::ServerError.new("Server error").tap { _1.response = res }
27
49
  when Net::HTTPUnauthorized
28
- raise LLM::UnauthorizedError.new { _1.response = res }, "Authentication error"
50
+ LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
29
51
  when Net::HTTPTooManyRequests
30
- raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
52
+ LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
31
53
  else
32
- raise LLM::Error.new { _1.response = res }, "Unexpected response"
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
- # bot = LLM::Bot.new(llm)
13
+ # ses = LLM::Session.new(llm)
14
14
  # file = llm.files.create file: "/books/goodread.pdf"
15
- # bot.chat ["Tell me about this PDF", file]
16
- # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
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
- # bot = LLM::Bot.new(llm)
14
- # bot.chat ["Tell me about this photo", File.open("/images/dog.jpg", "rb")]
15
- # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
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
- # bot = LLM::Bot.new(llm)
18
- # bot.chat ["Tell me about this photo", File.open("/images/cat.jpg", "rb")]
19
- # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
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
- raise LLM::ServerError.new { _1.response = res }, "Server error"
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
- raise LLM::UnauthorizedError.new { _1.response = res }, "Authentication error"
58
+ LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
31
59
  else
32
- raise LLM::Error.new { _1.response = res }, "Unexpected response"
60
+ LLM::Error.new("Unexpected response").tap { _1.response = res }
33
61
  end
34
62
  when Net::HTTPTooManyRequests
35
- raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
63
+ LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
36
64
  else
37
- raise LLM::Error.new { _1.response = res }, "Unexpected response"
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
- # bot = LLM::Bot.new(llm)
21
+ # ses = LLM::Session.new(llm)
22
22
  # file = llm.files.create(file: "/audio/haiku.mp3")
23
- # bot.chat ["Tell me about this file", file]
24
- # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
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
@@ -14,9 +14,9 @@ module LLM
14
14
  # require "llm"
15
15
  #
16
16
  # llm = LLM.gemini(key: ENV["KEY"])
17
- # bot = LLM::Bot.new(llm)
18
- # bot.chat ["Tell me about this photo", File.open("/images/horse.jpg", "rb")]
19
- # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
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
  ##