llm.rb 4.6.0 → 4.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c26db042940fc118505804bbec91d43dd39313b6625b4e3d7df51f43f01d58ae
4
- data.tar.gz: 5780f3e3d0db3579772d4f7ba141052314d34b7fd0db06ba029e4b4fd6fd84dc
3
+ metadata.gz: 3d4facfaa664a93ec948639191915c027613ed7bd407f38acb79182d4ca10c31
4
+ data.tar.gz: 5974b17ea6f4c1317ee3eca9dd7ba9f6e342423cb954fdcf5a7474b53b660e88
5
5
  SHA512:
6
- metadata.gz: 864ba517e81d8fb4d248d754c5a9fc4bdcbf162f9ac4fb7691a54c015006ed40035300fd19c2057f755678676534a568204b0e0bc29b8e058cf2334b8625465e
7
- data.tar.gz: cd8b18c4e780a26c83201ea39f81077ad2535db966efcb5fe860f4af55808f96a8bd8a6bdd4c25f38a9012b3092eb2611bbfd31552969b00b15579af5e95354a
6
+ metadata.gz: e5b10736981b4bc2b2c0e06e45c6174ba78c869f7d4666da924e893d5582eb133ac6900d46cf0429cc4936d1f4090e58ba4b3f4cf862604e6f1bea3b86c95d2e
7
+ data.tar.gz: 7a1ea1a6c30999f595139381c788aa4876d569ac03dde0cbfa9b1f302c7b5c6c0a7bc2c394ce754041d2aa417587b2d05f90092161cc974aa5fae5d332469002
data/README.md CHANGED
@@ -66,7 +66,7 @@ end
66
66
  llm = LLM.openai(key: ENV["KEY"])
67
67
  ses = LLM::Session.new(llm, schema: Report)
68
68
  res = ses.talk("Structure this report: 'Database latency spiked at 10:42 UTC, causing 5% request timeouts for 12 minutes.'")
69
- pp res.messages.first(&:assistant?).content!
69
+ pp res.content!
70
70
 
71
71
  ##
72
72
  # {
@@ -198,14 +198,14 @@ and share a provider across multiple threads:
198
198
  #!/usr/bin/env ruby
199
199
  require "llm"
200
200
 
201
- llm = LLM.openai(key: ENV["KEY"], persistent: true)
201
+ llm = LLM.openai(key: ENV["KEY"]).persist!
202
202
  schema = llm.schema.object(answer: llm.schema.integer.required)
203
203
 
204
204
  vals = 10.times.map do |x|
205
205
  Thread.new do
206
206
  ses = LLM::Session.new(llm, schema:)
207
207
  res = ses.talk "#{x} + 5 = ?"
208
- res.messages.find(&:assistant?).content!
208
+ res.content!
209
209
  end
210
210
  end.map(&:value)
211
211
 
@@ -335,7 +335,7 @@ and the gem should be installed separately:
335
335
  #!/usr/bin/env ruby
336
336
  require "llm"
337
337
 
338
- llm = LLM.openai(key: ENV["KEY"], persistent: true)
338
+ llm = LLM.openai(key: ENV["KEY"]).persist!
339
339
  res1 = llm.responses.create "message 1"
340
340
  res2 = llm.responses.create "message 2", previous_response_id: res1.response_id
341
341
  res3 = llm.responses.create "message 3", previous_response_id: res2.response_id
@@ -578,12 +578,13 @@ it has been uploaded. The file (a specialized instance of
578
578
  ```ruby
579
579
  #!/usr/bin/env ruby
580
580
  require "llm"
581
+ require "pp"
581
582
 
582
583
  llm = LLM.openai(key: ENV["KEY"])
583
584
  ses = LLM::Session.new(llm)
584
585
  file = llm.files.create(file: "/tmp/llm-book.pdf")
585
586
  res = ses.talk ["Tell me about this file", file]
586
- res.messages.each { |m| puts "[#{m.role}] #{m.content}" }
587
+ pp res.content
587
588
  ```
588
589
 
589
590
  ### Prompts
@@ -776,6 +777,7 @@ release:
776
777
  ```ruby
777
778
  #!/usr/bin/env ruby
778
779
  require "llm"
780
+ require "pp"
779
781
 
780
782
  ##
781
783
  # List all models
@@ -789,7 +791,7 @@ end
789
791
  model = llm.models.all.find { |m| m.id == "gpt-3.5-turbo" }
790
792
  ses = LLM::Session.new(llm, model: model.id)
791
793
  res = ses.talk "Hello #{model.id} :)"
792
- res.messages.each { |m| puts "[#{m.role}] #{m.content}" }
794
+ pp res.content
793
795
  ```
794
796
 
795
797
  ## Install
@@ -43,6 +43,20 @@ module LLM::Contract
43
43
  raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
44
44
  end
45
45
 
46
+ ##
47
+ # @return [String]
48
+ # Returns the LLM response
49
+ def content
50
+ messages.find(&:assistant?).content
51
+ end
52
+
53
+ ##
54
+ # @return [Hash]
55
+ # Returns the LLM response after parsing it as JSON
56
+ def content!
57
+ LLM.json.load(content)
58
+ end
59
+
46
60
  ##
47
61
  # @return [LLM::Usage]
48
62
  # Returns usage information
data/lib/llm/provider.rb CHANGED
@@ -289,6 +289,20 @@ class LLM::Provider
289
289
  end
290
290
  end
291
291
 
292
+ ##
293
+ # This method configures a provider to use a persistent connection pool
294
+ # via the optional dependency [Net::HTTP::Persistent](https://github.com/drbrain/net-http-persistent)
295
+ # @example
296
+ # llm = LLM.openai(key: ENV["KEY"]).persist!
297
+ # # do something with 'llm'
298
+ # @return [LLM::Provider]
299
+ def persist!
300
+ client = persistent_client
301
+ lock do
302
+ tap { @client = client }
303
+ end
304
+ end
305
+
292
306
  private
293
307
 
294
308
  attr_reader :client, :base_uri, :host, :port, :timeout, :ssl
@@ -45,6 +45,18 @@ module LLM::Anthropic::ResponseAdapter
45
45
  body.model
46
46
  end
47
47
 
48
+ ##
49
+ # (see LLM::Contract::Completion#content)
50
+ def content
51
+ super
52
+ end
53
+
54
+ ##
55
+ # (see LLM::Contract::Completion#content!)
56
+ def content!
57
+ super
58
+ end
59
+
48
60
  private
49
61
 
50
62
  def adapt_choices
@@ -45,6 +45,18 @@ module LLM::Gemini::ResponseAdapter
45
45
  body.modelVersion
46
46
  end
47
47
 
48
+ ##
49
+ # (see LLM::Contract::Completion#content)
50
+ def content
51
+ super
52
+ end
53
+
54
+ ##
55
+ # (see LLM::Contract::Completion#content!)
56
+ def content!
57
+ super
58
+ end
59
+
48
60
  private
49
61
 
50
62
  def adapt_choices
@@ -45,6 +45,18 @@ module LLM::Ollama::ResponseAdapter
45
45
  body.model
46
46
  end
47
47
 
48
+ ##
49
+ # (see LLM::Contract::Completion#content)
50
+ def content
51
+ super
52
+ end
53
+
54
+ ##
55
+ # (see LLM::Contract::Completion#content!)
56
+ def content!
57
+ super
58
+ end
59
+
48
60
  private
49
61
 
50
62
  def adapt_choices
@@ -57,6 +57,18 @@ module LLM::OpenAI::ResponseAdapter
57
57
  body.model
58
58
  end
59
59
 
60
+ ##
61
+ # (see LLM::Contract::Completion#content)
62
+ def content
63
+ super
64
+ end
65
+
66
+ ##
67
+ # (see LLM::Contract::Completion#content!)
68
+ def content!
69
+ super
70
+ end
71
+
60
72
  private
61
73
 
62
74
  def adapt_tool_calls(tools)
@@ -48,6 +48,7 @@ module LLM
48
48
  def initialize(provider, options = {})
49
49
  super
50
50
  @exporter = options.delete(:exporter)
51
+ setup_langsmith!(options.delete(:langsmith))
51
52
  setup!
52
53
  end
53
54
 
@@ -132,7 +133,7 @@ module LLM
132
133
  "gen_ai.provider.name" => provider_name,
133
134
  "server.address" => provider_host,
134
135
  "server.port" => provider_port
135
- }.compact
136
+ }.merge!(langsmith_attributes(span_kind: "tool")).compact
136
137
  span_name = ["execute_tool", name].compact.join(" ")
137
138
  span = create_span(span_name.empty? ? "gen_ai.tool" : span_name, attributes:)
138
139
  span.add_event("gen_ai.tool.start")
@@ -288,7 +289,7 @@ module LLM
288
289
  "gen_ai.provider.name" => provider_name,
289
290
  "server.address" => provider_host,
290
291
  "server.port" => provider_port
291
- }.compact
292
+ }.merge!(langsmith_attributes(span_kind: "llm")).compact
292
293
  span_name = [operation, model].compact.join(" ")
293
294
  span = create_span(span_name.empty? ? "gen_ai.request" : span_name, attributes:)
294
295
  span.add_event("gen_ai.request.start")
@@ -301,7 +302,7 @@ module LLM
301
302
  "gen_ai.provider.name" => provider_name,
302
303
  "server.address" => provider_host,
303
304
  "server.port" => provider_port
304
- }.compact
305
+ }.merge!(langsmith_attributes(span_kind: "retriever")).compact
305
306
  span = create_span(operation, attributes:)
306
307
  span.add_event("gen_ai.request.start")
307
308
  span
@@ -332,5 +333,53 @@ module LLM
332
333
  span.add_event("gen_ai.request.finish")
333
334
  span.tap(&:finish)
334
335
  end
336
+
337
+ def setup_langsmith!(options)
338
+ options ||= {}
339
+ @langsmith_metadata = options[:metadata] || {}
340
+ @langsmith_session_id = normalize_langsmith_session_id(options[:session_id], metadata: @langsmith_metadata)
341
+ @langsmith_tags = options[:tags] || []
342
+ end
343
+
344
+ def langsmith_attributes(span_kind:)
345
+ attributes = {}
346
+ unless @langsmith_session_id.to_s.empty?
347
+ attributes["langsmith.trace.session_id"] = @langsmith_session_id
348
+ end
349
+ @langsmith_metadata.each do |key, value|
350
+ next if value.nil?
351
+
352
+ attributes["langsmith.metadata.#{key}"] = serialize_langsmith_value(value)
353
+ end
354
+ unless @langsmith_tags.empty?
355
+ attributes["langsmith.span.tags"] = @langsmith_tags.map(&:to_s).join(",")
356
+ end
357
+ attributes["langsmith.span.kind"] = span_kind
358
+ attributes
359
+ end
360
+
361
+ def serialize_langsmith_value(value)
362
+ case value
363
+ when String, Numeric, TrueClass, FalseClass
364
+ value
365
+ else
366
+ LLM.json.dump(value)
367
+ end
368
+ end
369
+
370
+ def normalize_langsmith_session_id(session_id, metadata:)
371
+ raw = session_id&.to_s
372
+ return nil if raw.to_s.empty?
373
+ return raw if uuid?(raw)
374
+
375
+ # Keep arbitrary thread identifiers in metadata instead of forcing
376
+ # them into langsmith.trace.session_id, which expects a known UUID.
377
+ metadata[:session_id] ||= raw
378
+ nil
379
+ end
380
+
381
+ def uuid?(value)
382
+ value.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i)
383
+ end
335
384
  end
336
385
  end
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "4.6.0"
4
+ VERSION = "4.7.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.6.0
4
+ version: 4.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antar Azri