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 +4 -4
- data/README.md +8 -6
- data/lib/llm/contract/completion.rb +14 -0
- data/lib/llm/provider.rb +14 -0
- data/lib/llm/providers/anthropic/response_adapter/completion.rb +12 -0
- data/lib/llm/providers/gemini/response_adapter/completion.rb +12 -0
- data/lib/llm/providers/ollama/response_adapter/completion.rb +12 -0
- data/lib/llm/providers/openai/response_adapter/completion.rb +12 -0
- data/lib/llm/tracer/telemetry.rb +52 -3
- data/lib/llm/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d4facfaa664a93ec948639191915c027613ed7bd407f38acb79182d4ca10c31
|
|
4
|
+
data.tar.gz: 5974b17ea6f4c1317ee3eca9dd7ba9f6e342423cb954fdcf5a7474b53b660e88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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"]
|
|
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.
|
|
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"]
|
|
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.
|
|
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.
|
|
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)
|
data/lib/llm/tracer/telemetry.rb
CHANGED
|
@@ -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