llm.rb 11.3.0 → 12.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/CHANGELOG.md +270 -1
- data/LICENSE +92 -17
- data/README.md +209 -647
- data/data/anthropic.json +433 -249
- data/data/bedrock.json +2097 -1055
- data/data/deepinfra.json +993 -0
- data/data/deepseek.json +53 -28
- data/data/google.json +389 -771
- data/data/openai.json +1053 -771
- data/data/xai.json +133 -292
- data/data/zai.json +249 -141
- data/lib/llm/a2a.rb +2 -2
- data/lib/llm/active_record/acts_as_agent.rb +10 -41
- data/lib/llm/active_record/acts_as_llm.rb +25 -0
- data/lib/llm/active_record.rb +3 -3
- data/lib/llm/agent.rb +1 -1
- data/lib/llm/context.rb +9 -5
- data/lib/llm/contract/completion.rb +2 -2
- data/lib/llm/cost.rb +1 -1
- data/lib/llm/function/fiber_group.rb +2 -2
- data/lib/llm/function/task_group.rb +2 -2
- data/lib/llm/function/thread_group.rb +3 -3
- data/lib/llm/pipe.rb +1 -1
- data/lib/llm/provider.rb +2 -2
- data/lib/llm/providers/anthropic/request_adapter.rb +1 -1
- data/lib/llm/providers/bedrock/request_adapter/completion.rb +5 -5
- data/lib/llm/providers/bedrock/request_adapter.rb +3 -3
- data/lib/llm/providers/bedrock/response_adapter/completion.rb +2 -2
- data/lib/llm/providers/bedrock/response_adapter.rb +2 -2
- data/lib/llm/providers/deepinfra/audio.rb +66 -0
- data/lib/llm/providers/deepinfra/images.rb +90 -0
- data/lib/llm/providers/deepinfra/response_adapter.rb +36 -0
- data/lib/llm/providers/deepinfra.rb +100 -0
- data/lib/llm/providers/deepseek/images.rb +109 -0
- data/lib/llm/providers/deepseek/request_adapter.rb +32 -0
- data/lib/llm/providers/deepseek/response_adapter/image.rb +9 -0
- data/lib/llm/providers/deepseek/response_adapter.rb +29 -0
- data/lib/llm/providers/deepseek.rb +4 -2
- data/lib/llm/providers/google/request_adapter.rb +23 -6
- data/lib/llm/providers/google.rb +4 -4
- data/lib/llm/providers/ollama/request_adapter.rb +1 -1
- data/lib/llm/providers/openai/audio.rb +6 -2
- data/lib/llm/providers/openai/images.rb +9 -50
- data/lib/llm/providers/openai/request_adapter/respond.rb +38 -4
- data/lib/llm/providers/openai/request_adapter.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/audio.rb +5 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/image.rb +0 -4
- data/lib/llm/providers/openai/responses.rb +1 -0
- data/lib/llm/providers/openai/stream_parser.rb +5 -6
- data/lib/llm/providers/openai.rb +2 -2
- data/lib/llm/providers/xai/images.rb +49 -26
- data/lib/llm/providers/xai.rb +2 -2
- data/lib/llm/registry.rb +2 -2
- data/lib/llm/response.rb +11 -1
- data/lib/llm/schema/leaf.rb +7 -1
- data/lib/llm/schema/object.rb +1 -1
- data/lib/llm/schema/renderer.rb +121 -0
- data/lib/llm/schema.rb +30 -0
- data/lib/llm/sequel/agent.rb +2 -43
- data/lib/llm/sequel/plugin.rb +25 -7
- data/lib/llm/stream.rb +1 -1
- data/lib/llm/tool.rb +2 -2
- data/lib/llm/tracer/telemetry.rb +4 -6
- data/lib/llm/tracer.rb +9 -21
- data/lib/llm/transport/execution.rb +16 -1
- data/lib/llm/transport/http.rb +2 -2
- data/lib/llm/transport/net_http_adapter.rb +1 -1
- data/lib/llm/transport/persistent_http.rb +1 -1
- data/lib/llm/transport/response/http.rb +1 -1
- data/lib/llm/uridata.rb +16 -0
- data/lib/llm/utils.rb +1 -1
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +20 -8
- data/llm.gemspec +14 -26
- data/resources/deepdive.md +966 -0
- metadata +26 -29
- data/lib/llm/tracer/langsmith.rb +0 -144
|
@@ -4,30 +4,21 @@ class LLM::XAI
|
|
|
4
4
|
##
|
|
5
5
|
# The {LLM::XAI::Images LLM::XAI::Images} class provides an interface
|
|
6
6
|
# for [xAI's images API](https://docs.x.ai/docs/guides/image-generations).
|
|
7
|
-
# xAI
|
|
8
|
-
# encoded in base64. The default is to return base64-encoded image data.
|
|
7
|
+
# xAI returns base64-encoded image data.
|
|
9
8
|
#
|
|
10
|
-
# @example
|
|
9
|
+
# @example
|
|
11
10
|
# #!/usr/bin/env ruby
|
|
12
11
|
# require "llm"
|
|
13
|
-
# require "open-uri"
|
|
14
|
-
# require "fileutils"
|
|
15
12
|
#
|
|
16
13
|
# llm = LLM.xai(key: ENV["KEY"])
|
|
17
|
-
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
|
18
|
-
# response_format: "url"
|
|
19
|
-
# FileUtils.mv OpenURI.open_uri(res.urls[0]).path,
|
|
20
|
-
# "rocket.png"
|
|
21
|
-
#
|
|
22
|
-
# @example Binary strings
|
|
23
|
-
# #!/usr/bin/env ruby
|
|
24
|
-
# require "llm"
|
|
25
|
-
#
|
|
26
|
-
# llm = LLM.xai(key: ENV["KEY"])
|
|
27
|
-
# res = llm.images.create prompt: "A dog on a rocket to the moon",
|
|
28
|
-
# response_format: "b64_json"
|
|
14
|
+
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
|
29
15
|
# IO.copy_stream res.images[0], "rocket.png"
|
|
30
16
|
class Images < LLM::OpenAI::Images
|
|
17
|
+
##
|
|
18
|
+
# @api private
|
|
19
|
+
PATTERN = %r{\A(?:https?://|data:)}
|
|
20
|
+
private_constant :PATTERN
|
|
21
|
+
|
|
31
22
|
##
|
|
32
23
|
# Create an image
|
|
33
24
|
# @example
|
|
@@ -40,20 +31,52 @@ class LLM::XAI
|
|
|
40
31
|
# @param [Hash] params Other parameters (see xAI docs)
|
|
41
32
|
# @raise (see LLM::Provider#request)
|
|
42
33
|
# @return [LLM::Response]
|
|
43
|
-
def create(prompt:, model: "grok-imagine-image", **params)
|
|
44
|
-
|
|
34
|
+
def create(prompt:, model: "grok-imagine-image-quality", **params)
|
|
35
|
+
req = LLM::Transport::Request.post(path("/images/generations"), headers)
|
|
36
|
+
req.body = LLM.json.dump({prompt:, n: 1, model:, response_format: "b64_json"}.merge!(params))
|
|
37
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
38
|
+
res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
|
|
39
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
40
|
+
res
|
|
45
41
|
end
|
|
46
42
|
|
|
47
43
|
##
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
# Edit an image
|
|
45
|
+
# @example
|
|
46
|
+
# llm = LLM.xai(key: ENV["KEY"])
|
|
47
|
+
# res = llm.images.edit(image: "/images/book.png", prompt: "The book is floating in the clouds")
|
|
48
|
+
# IO.copy_stream res.images[0], "floating-book.png"
|
|
49
|
+
# @see https://docs.x.ai/docs/guides/image-generations xAI docs
|
|
50
|
+
# @param [String, LLM::File, File] image The image to edit
|
|
51
|
+
# @param [String] prompt The prompt
|
|
52
|
+
# @param [String] model The model to use
|
|
53
|
+
# @param [Hash] params Other parameters (see xAI docs)
|
|
54
|
+
# @raise (see LLM::Provider#request)
|
|
55
|
+
# @return [LLM::Response]
|
|
56
|
+
def edit(image:, prompt:, model: "grok-imagine-image-quality", **params)
|
|
57
|
+
req = LLM::Transport::Request.post(path("/images/edits"), headers)
|
|
58
|
+
req.body = LLM.json.dump({
|
|
59
|
+
prompt:,
|
|
60
|
+
model:,
|
|
61
|
+
image: image_url(image),
|
|
62
|
+
response_format: "b64_json"
|
|
63
|
+
}.merge!(params))
|
|
64
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
65
|
+
res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
|
|
66
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
67
|
+
res
|
|
51
68
|
end
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def
|
|
56
|
-
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def image_url(image)
|
|
73
|
+
case image
|
|
74
|
+
when String
|
|
75
|
+
url = image.match?(PATTERN) ? image : LLM.File(image).to_data_uri
|
|
76
|
+
else
|
|
77
|
+
url = LLM.File(image).to_data_uri
|
|
78
|
+
end
|
|
79
|
+
{url:, type: "image_url"}
|
|
57
80
|
end
|
|
58
81
|
end
|
|
59
82
|
end
|
data/lib/llm/providers/xai.rb
CHANGED
|
@@ -70,10 +70,10 @@ module LLM
|
|
|
70
70
|
|
|
71
71
|
##
|
|
72
72
|
# Returns the default model for chat completions
|
|
73
|
-
# #see https://docs.x.ai/docs/models grok-4
|
|
73
|
+
# #see https://docs.x.ai/docs/models grok-4.3
|
|
74
74
|
# @return [String]
|
|
75
75
|
def default_model
|
|
76
|
-
"grok-4
|
|
76
|
+
"grok-4.3"
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
end
|
data/lib/llm/registry.rb
CHANGED
|
@@ -12,7 +12,7 @@ class LLM::Registry
|
|
|
12
12
|
##
|
|
13
13
|
# @raise [LLM::Error]
|
|
14
14
|
# Might raise an error
|
|
15
|
-
# @param [Symbol]
|
|
15
|
+
# @param [Symbol] name
|
|
16
16
|
# A provider name
|
|
17
17
|
# @return [LLM::Registry]
|
|
18
18
|
def self.for(name)
|
|
@@ -71,7 +71,7 @@ class LLM::Registry
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
##
|
|
74
|
-
# Similar to
|
|
74
|
+
# Similar to `#find` but returns the block's return value
|
|
75
75
|
# @return [Object, nil]
|
|
76
76
|
def find_map(pair)
|
|
77
77
|
result = nil
|
data/lib/llm/response.rb
CHANGED
|
@@ -14,7 +14,7 @@ module LLM
|
|
|
14
14
|
# through {#res}. When the default net/http transport is in use,
|
|
15
15
|
# {LLM::Transport::Response::HTTP
|
|
16
16
|
# LLM::Transport::Response::HTTP} keeps the
|
|
17
|
-
# original
|
|
17
|
+
# original `Net::HTTPResponse` available through
|
|
18
18
|
# its own {LLM::Transport::Response::HTTP#res #res}.
|
|
19
19
|
class Response
|
|
20
20
|
require "json"
|
|
@@ -56,6 +56,16 @@ module LLM
|
|
|
56
56
|
@res.success?
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
##
|
|
60
|
+
# Returns the provider response id when present.
|
|
61
|
+
# @return [String, nil]
|
|
62
|
+
def id
|
|
63
|
+
return nil unless LLM::Object === body
|
|
64
|
+
body.id ||
|
|
65
|
+
body.responseId || body.response_id ||
|
|
66
|
+
body.requestId || body.request_id
|
|
67
|
+
end
|
|
68
|
+
|
|
59
69
|
##
|
|
60
70
|
# Returns true if the response is from the Files API
|
|
61
71
|
# @return [Boolean]
|
data/lib/llm/schema/leaf.rb
CHANGED
|
@@ -95,7 +95,7 @@ class LLM::Schema
|
|
|
95
95
|
##
|
|
96
96
|
# @return [Hash]
|
|
97
97
|
def to_h
|
|
98
|
-
{description: @description, default: @default, enum: @enum}.compact
|
|
98
|
+
{description: @description, default: @default, enum: @enum, const: @const}.compact
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
##
|
|
@@ -104,6 +104,12 @@ class LLM::Schema
|
|
|
104
104
|
to_h.to_json(options)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
##
|
|
108
|
+
# @return [String]
|
|
109
|
+
def to_s
|
|
110
|
+
LLM::Schema::Renderer.render(self)
|
|
111
|
+
end
|
|
112
|
+
|
|
107
113
|
##
|
|
108
114
|
# @param [LLM::Schema::Leaf] other
|
|
109
115
|
# An object to compare
|
data/lib/llm/schema/object.rb
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Schema
|
|
4
|
+
##
|
|
5
|
+
# Internal renderer for prompt-friendly schema output.
|
|
6
|
+
# @api private
|
|
7
|
+
module Renderer
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# Render a schema node as a human-readable string.
|
|
12
|
+
# @param [LLM::Schema::Leaf] node
|
|
13
|
+
# The schema node to render
|
|
14
|
+
# @param [Integer] indent
|
|
15
|
+
# The indentation level
|
|
16
|
+
# @param [String, Symbol, nil] name
|
|
17
|
+
# The property name for nested nodes
|
|
18
|
+
# @param [Boolean] root
|
|
19
|
+
# Whether the node is the root schema object
|
|
20
|
+
# @return [String]
|
|
21
|
+
def render(node, indent: 0, name: nil, root: false)
|
|
22
|
+
line = (" " * indent).to_s
|
|
23
|
+
if name
|
|
24
|
+
line << name.to_s
|
|
25
|
+
line << "?" unless node.required?
|
|
26
|
+
line << ": "
|
|
27
|
+
end
|
|
28
|
+
line << type_name(node)
|
|
29
|
+
metadata = metadata_for(node, include_required: !root)
|
|
30
|
+
line << " (#{metadata.join(", ")})" unless metadata.empty?
|
|
31
|
+
line << " - #{node.description}" if node.respond_to?(:description) && node.description
|
|
32
|
+
nested = nested_lines(node, indent: indent + 2)
|
|
33
|
+
([line] + nested).join("\n")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# Render nested lines for compound schema nodes.
|
|
40
|
+
# @param [LLM::Schema::Leaf] node
|
|
41
|
+
# The schema node
|
|
42
|
+
# @param [Integer] indent
|
|
43
|
+
# The indentation level
|
|
44
|
+
# @return [Array<String>]
|
|
45
|
+
def nested_lines(node, indent:)
|
|
46
|
+
case node
|
|
47
|
+
when LLM::Schema::Object
|
|
48
|
+
node.properties.map { |key, val| render(val, indent:, name: key) }
|
|
49
|
+
when LLM::Schema::Array
|
|
50
|
+
items = node.to_h[:items]
|
|
51
|
+
items.is_a?(LLM::Schema::Object) ? [render(items, indent:, name: "items")] : []
|
|
52
|
+
else
|
|
53
|
+
[]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Return the printable type name for a schema node.
|
|
59
|
+
# @param [LLM::Schema::Leaf] node
|
|
60
|
+
# The schema node
|
|
61
|
+
# @return [String]
|
|
62
|
+
def type_name(node)
|
|
63
|
+
h = node.to_h
|
|
64
|
+
return "array<#{inline_type(h[:items])}>" if node.is_a?(LLM::Schema::Array)
|
|
65
|
+
return "anyOf<#{inline_types(h[:anyOf])}>" if node.is_a?(LLM::Schema::AnyOf)
|
|
66
|
+
return "oneOf<#{inline_types(h[:oneOf])}>" if node.is_a?(LLM::Schema::OneOf)
|
|
67
|
+
return "allOf<#{inline_types(h[:allOf])}>" if node.is_a?(LLM::Schema::AllOf)
|
|
68
|
+
h[:type] || "unknown"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Return the inline type description for a nested node.
|
|
73
|
+
# @param [LLM::Schema::Leaf, Object] node
|
|
74
|
+
# The nested schema node
|
|
75
|
+
# @return [String]
|
|
76
|
+
def inline_type(node)
|
|
77
|
+
return type_name(node) if node.is_a?(LLM::Schema::Leaf)
|
|
78
|
+
node.inspect
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
##
|
|
82
|
+
# Return the inline type description for a list of nodes.
|
|
83
|
+
# @param [Array<LLM::Schema::Leaf>] values
|
|
84
|
+
# The union members
|
|
85
|
+
# @return [String]
|
|
86
|
+
def inline_types(values)
|
|
87
|
+
values.map { inline_type(_1) }.join(", ")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Extract printable metadata for a schema node.
|
|
92
|
+
# @param [LLM::Schema::Leaf] node
|
|
93
|
+
# The schema node
|
|
94
|
+
# @param [Boolean] include_required
|
|
95
|
+
# Whether to include the required marker
|
|
96
|
+
# @return [Array<String>]
|
|
97
|
+
def metadata_for(node, include_required:)
|
|
98
|
+
h = node.to_h.dup
|
|
99
|
+
details = []
|
|
100
|
+
details << "required" if include_required && node.required?
|
|
101
|
+
details << "default: #{value(node.default)}" if node.default
|
|
102
|
+
details << "enum: #{node.enum.map { value(_1) }.join(" | ")}" if node.enum
|
|
103
|
+
details << "const: #{value(node.const)}" if node.const
|
|
104
|
+
h.except(:type, :description, :default, :enum, :const, :required, :properties, :items, :anyOf, :oneOf, :allOf)
|
|
105
|
+
.each { |key, val| details << "#{key}: #{value(val)}" }
|
|
106
|
+
details
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Convert a scalar value into its printable representation.
|
|
111
|
+
# @param [Object] val
|
|
112
|
+
# The value to render
|
|
113
|
+
# @return [String]
|
|
114
|
+
def value(val)
|
|
115
|
+
case val
|
|
116
|
+
when ::String then val.inspect
|
|
117
|
+
else val.to_s
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/llm/schema.rb
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
class LLM::Schema
|
|
35
35
|
require_relative "schema/version"
|
|
36
36
|
require_relative "schema/parser"
|
|
37
|
+
require_relative "schema/renderer"
|
|
37
38
|
require_relative "schema/leaf"
|
|
38
39
|
require_relative "schema/object"
|
|
39
40
|
require_relative "schema/array"
|
|
@@ -121,6 +122,19 @@ class LLM::Schema
|
|
|
121
122
|
end
|
|
122
123
|
end
|
|
123
124
|
|
|
125
|
+
##
|
|
126
|
+
# @param [Hash] defaults
|
|
127
|
+
# @return [LLM::Schema::Object]
|
|
128
|
+
def self.defaults(defaults)
|
|
129
|
+
lock do
|
|
130
|
+
object.tap do |schema|
|
|
131
|
+
defaults.each do |name, val|
|
|
132
|
+
Utils.fetch(schema.properties, name).default(val)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
124
138
|
##
|
|
125
139
|
# @api private
|
|
126
140
|
# @return [LLM::Schema]
|
|
@@ -139,6 +153,14 @@ class LLM::Schema
|
|
|
139
153
|
end
|
|
140
154
|
end
|
|
141
155
|
|
|
156
|
+
##
|
|
157
|
+
# Render the schema as a prompt-friendly string.
|
|
158
|
+
# @return [String]
|
|
159
|
+
def self.to_s
|
|
160
|
+
Renderer.render(object, root: true)
|
|
161
|
+
end
|
|
162
|
+
(class << self; self; end).alias_method(:inspect, :to_s)
|
|
163
|
+
|
|
142
164
|
##
|
|
143
165
|
# @api private
|
|
144
166
|
def self.lock(&)
|
|
@@ -220,4 +242,12 @@ class LLM::Schema
|
|
|
220
242
|
def null
|
|
221
243
|
Null.new
|
|
222
244
|
end
|
|
245
|
+
|
|
246
|
+
##
|
|
247
|
+
# Render a schema leaf as a prompt-friendly string.
|
|
248
|
+
# @return [String]
|
|
249
|
+
def to_s
|
|
250
|
+
self.class.to_s
|
|
251
|
+
end
|
|
252
|
+
alias_method :inspect, :to_s
|
|
223
253
|
end
|
data/lib/llm/sequel/agent.rb
CHANGED
|
@@ -7,8 +7,7 @@ module LLM::Sequel
|
|
|
7
7
|
# This wrapper reuses the same record-backed runtime surface as
|
|
8
8
|
# {LLM::Sequel::Plugin}, but builds an {LLM::Agent LLM::Agent} instead of an
|
|
9
9
|
# {LLM::Context LLM::Context}. Agent defaults such as model, tools, schema,
|
|
10
|
-
# instructions, and concurrency are configured on
|
|
11
|
-
# forwarded to an internal agent subclass.
|
|
10
|
+
# instructions, and concurrency are configured on an internal agent subclass.
|
|
12
11
|
module Agent
|
|
13
12
|
require_relative "plugin"
|
|
14
13
|
EMPTY_HASH = LLM::Sequel::Plugin::EMPTY_HASH
|
|
@@ -25,7 +24,7 @@ module LLM::Sequel
|
|
|
25
24
|
options = DEFAULTS.merge(options)
|
|
26
25
|
model.db.extension :pg_json if %i[json jsonb].include?(options[:format])
|
|
27
26
|
model.instance_variable_set(:@llm_agent_options, options.freeze)
|
|
28
|
-
model.instance_exec(&block)
|
|
27
|
+
block_given? ? model.instance_exec(model.agent, &block) : nil
|
|
29
28
|
end
|
|
30
29
|
|
|
31
30
|
module ClassMethods
|
|
@@ -33,46 +32,6 @@ module LLM::Sequel
|
|
|
33
32
|
@llm_agent_options || Agent::DEFAULTS
|
|
34
33
|
end
|
|
35
34
|
|
|
36
|
-
def model(model = nil, &block)
|
|
37
|
-
return agent.model if model.nil? && !block
|
|
38
|
-
agent.model(model, &block)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def tools(*tools, &block)
|
|
42
|
-
return agent.tools if tools.empty? && !block
|
|
43
|
-
agent.tools(*tools, &block)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def skills(*skills, &block)
|
|
47
|
-
return agent.skills if skills.empty? && !block
|
|
48
|
-
agent.skills(*skills, &block)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def schema(schema = nil, &block)
|
|
52
|
-
return agent.schema if schema.nil? && !block
|
|
53
|
-
agent.schema(schema, &block)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def instructions(instructions = nil)
|
|
57
|
-
return agent.instructions if instructions.nil?
|
|
58
|
-
agent.instructions(instructions)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def concurrency(concurrency = nil)
|
|
62
|
-
return agent.concurrency if concurrency.nil?
|
|
63
|
-
agent.concurrency(concurrency)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def confirm(*tool_names, &block)
|
|
67
|
-
return agent.confirm if tool_names.empty? && !block
|
|
68
|
-
agent.confirm(*tool_names, &block)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def tracer(tracer = nil, &block)
|
|
72
|
-
return agent.tracer if tracer.nil? && !block
|
|
73
|
-
agent.tracer(tracer, &block)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
35
|
def agent
|
|
77
36
|
@agent ||= Class.new(LLM::Agent)
|
|
78
37
|
end
|
data/lib/llm/sequel/plugin.rb
CHANGED
|
@@ -16,6 +16,13 @@ module LLM::Sequel
|
|
|
16
16
|
# JSON typecasting for the model. `provider:`, `context:`, and `tracer:`
|
|
17
17
|
# can also be configured as symbols that are called on the model.
|
|
18
18
|
module Plugin
|
|
19
|
+
DEFAULTS = {
|
|
20
|
+
data_column: :data,
|
|
21
|
+
format: :string,
|
|
22
|
+
provider: :set_provider,
|
|
23
|
+
context: :set_context,
|
|
24
|
+
tracer: :set_tracer
|
|
25
|
+
}.freeze
|
|
19
26
|
EMPTY_HASH = {}.freeze
|
|
20
27
|
|
|
21
28
|
##
|
|
@@ -94,13 +101,6 @@ module LLM::Sequel
|
|
|
94
101
|
end
|
|
95
102
|
end
|
|
96
103
|
end
|
|
97
|
-
DEFAULTS = {
|
|
98
|
-
data_column: :data,
|
|
99
|
-
format: :string,
|
|
100
|
-
tracer: nil,
|
|
101
|
-
provider: nil,
|
|
102
|
-
context: EMPTY_HASH
|
|
103
|
-
}.freeze
|
|
104
104
|
|
|
105
105
|
##
|
|
106
106
|
# Called by Sequel when the plugin is first applied to a model class.
|
|
@@ -304,6 +304,24 @@ module LLM::Sequel
|
|
|
304
304
|
|
|
305
305
|
private
|
|
306
306
|
|
|
307
|
+
##
|
|
308
|
+
# @return [LLM::Provider]
|
|
309
|
+
def set_provider
|
|
310
|
+
raise NotImplementedError, "implement the set_provider callback"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
##
|
|
314
|
+
# @return [Hash]
|
|
315
|
+
def set_context
|
|
316
|
+
Plugin::EMPTY_HASH.dup
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
##
|
|
320
|
+
# @return [LLM::Tracer]
|
|
321
|
+
def set_tracer
|
|
322
|
+
nil
|
|
323
|
+
end
|
|
324
|
+
|
|
307
325
|
##
|
|
308
326
|
# @return [LLM::Context]
|
|
309
327
|
def ctx
|
data/lib/llm/stream.rb
CHANGED
|
@@ -15,7 +15,7 @@ module LLM
|
|
|
15
15
|
# therefore block streaming progress and should generally return as
|
|
16
16
|
# quickly as possible.
|
|
17
17
|
#
|
|
18
|
-
# The most common callback is {#on_content}, which also maps to
|
|
18
|
+
# The most common callback is {#on_content}, which also maps to `#<<`.
|
|
19
19
|
# Providers may also call {#on_reasoning_content} and {#on_tool_call} when
|
|
20
20
|
# that data is available. Runtime features such as context compaction may
|
|
21
21
|
# also emit lifecycle callbacks like {#on_transform} or {#on_compaction}.
|
data/lib/llm/tool.rb
CHANGED
|
@@ -63,7 +63,7 @@ class LLM::Tool
|
|
|
63
63
|
##
|
|
64
64
|
# @param [LLM::A2A] a2a
|
|
65
65
|
# The A2A client that will execute the tool call
|
|
66
|
-
# @param [LLM::A2A::Card::Skill]
|
|
66
|
+
# @param [LLM::A2A::Card::Skill] skill
|
|
67
67
|
# An A2A tool
|
|
68
68
|
# @return [Class<LLM::Tool>]
|
|
69
69
|
# Returns a subclass of LLM::Tool
|
|
@@ -124,7 +124,7 @@ class LLM::Tool
|
|
|
124
124
|
|
|
125
125
|
##
|
|
126
126
|
# Registers the tool as a function when inherited
|
|
127
|
-
# @param [Class]
|
|
127
|
+
# @param [Class] tool The subclass
|
|
128
128
|
# @return [void]
|
|
129
129
|
def self.inherited(tool)
|
|
130
130
|
LLM.lock(:inherited) do
|
data/lib/llm/tracer/telemetry.rb
CHANGED
|
@@ -30,8 +30,7 @@ module LLM
|
|
|
30
30
|
# require "llm"
|
|
31
31
|
# require "opentelemetry-exporter-otlp"
|
|
32
32
|
#
|
|
33
|
-
#
|
|
34
|
-
# exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint:)
|
|
33
|
+
# exporter = OpenTelemetry::Exporter::OTLP::Exporter.new
|
|
35
34
|
#
|
|
36
35
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
37
36
|
# llm.tracer = LLM::Tracer::Telemetry.new(llm, exporter:)
|
|
@@ -59,7 +58,6 @@ module LLM
|
|
|
59
58
|
# @return [self]
|
|
60
59
|
def start_trace(trace_group_id: nil, name: "llm", attributes: {}, metadata: nil)
|
|
61
60
|
return self if trace_group_id.to_s.empty?
|
|
62
|
-
|
|
63
61
|
span_context = span_context_from_trace_group_id(trace_group_id.to_s)
|
|
64
62
|
parent_ctx = ::OpenTelemetry::Trace.context_with_span(
|
|
65
63
|
::OpenTelemetry::Trace.non_recording_span(span_context)
|
|
@@ -316,7 +314,7 @@ module LLM
|
|
|
316
314
|
set_span_attributes(span, consume_extra_outputs.merge(outputs || {}))
|
|
317
315
|
finish_metadata = consume_finish_metadata_proc(res)
|
|
318
316
|
metadata = (metadata || {}).merge(finish_metadata || {})
|
|
319
|
-
set_span_attributes(span, metadata.transform_keys { "
|
|
317
|
+
set_span_attributes(span, metadata.transform_keys { "llm.metadata.#{_1}" })
|
|
320
318
|
span.add_event("gen_ai.request.finish")
|
|
321
319
|
span.tap(&:finish)
|
|
322
320
|
end
|
|
@@ -326,7 +324,7 @@ module LLM
|
|
|
326
324
|
"gen_ai.operation.name" => operation
|
|
327
325
|
}.merge!(finish_attributes(operation, res)).compact
|
|
328
326
|
chunks_json = retrieval_chunks_json(res)
|
|
329
|
-
attributes["
|
|
327
|
+
attributes["llm.metadata.chunks"] = chunks_json if chunks_json
|
|
330
328
|
attributes.each { span.set_attribute(_1, _2) }
|
|
331
329
|
span.add_event("gen_ai.request.finish")
|
|
332
330
|
span.tap(&:finish)
|
|
@@ -334,7 +332,7 @@ module LLM
|
|
|
334
332
|
|
|
335
333
|
##
|
|
336
334
|
# @api private
|
|
337
|
-
# Serialize retrieval response chunks for span attributes
|
|
335
|
+
# Serialize retrieval response chunks for span attributes.
|
|
338
336
|
# Returns a JSON string or nil when res has no data.
|
|
339
337
|
def consume_finish_metadata_proc(res)
|
|
340
338
|
key = LLM::Tracer::FINISH_METADATA_PROC_KEY
|
data/lib/llm/tracer.rb
CHANGED
|
@@ -11,7 +11,6 @@ module LLM
|
|
|
11
11
|
class Tracer
|
|
12
12
|
require_relative "tracer/logger"
|
|
13
13
|
require_relative "tracer/telemetry"
|
|
14
|
-
require_relative "tracer/langsmith"
|
|
15
14
|
require_relative "tracer/null"
|
|
16
15
|
|
|
17
16
|
##
|
|
@@ -45,7 +44,7 @@ module LLM
|
|
|
45
44
|
# @param [Object, nil] span
|
|
46
45
|
# @param [String] model
|
|
47
46
|
# @param [Hash, nil] outputs Optional span attributes (e.g. gen_ai.output.messages) from llm.rb or caller.
|
|
48
|
-
# @param [Hash, nil] metadata Optional metadata
|
|
47
|
+
# @param [Hash, nil] metadata Optional metadata from llm.rb or caller.
|
|
49
48
|
# @return [void]
|
|
50
49
|
def on_request_finish(operation:, res:, model: nil, span: nil, outputs: nil, metadata: nil)
|
|
51
50
|
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
@@ -110,8 +109,7 @@ module LLM
|
|
|
110
109
|
# @param [Hash] attributes
|
|
111
110
|
# OpenTelemetry attributes to set on the root span.
|
|
112
111
|
# @param [Hash, nil] metadata
|
|
113
|
-
# Optional. Trace-level metadata merged into the trace
|
|
114
|
-
# Only used by tracers that support it (e.g. {LLM::Tracer::Langsmith}).
|
|
112
|
+
# Optional. Trace-level metadata merged into the trace by tracers that support it.
|
|
115
113
|
# @return [self]
|
|
116
114
|
def start_trace(trace_group_id: nil, name: "llm", attributes: {}, metadata: nil)
|
|
117
115
|
self
|
|
@@ -150,11 +148,10 @@ module LLM
|
|
|
150
148
|
##
|
|
151
149
|
# Merges extra attributes for the current trace/span. Used by applications
|
|
152
150
|
# (e.g. chatbot) to add metadata, span inputs, or span outputs to the next
|
|
153
|
-
# span or to the trace. No-op by default
|
|
154
|
-
# into fiber-local storage and emits them as langsmith/GenAI attributes.
|
|
151
|
+
# span or to the trace. No-op by default.
|
|
155
152
|
#
|
|
156
153
|
# @param [Hash, nil] metadata
|
|
157
|
-
# Key-value pairs merged into trace/span metadata
|
|
154
|
+
# Key-value pairs merged into trace/span metadata.
|
|
158
155
|
# @param [Hash, nil] inputs
|
|
159
156
|
# Key-value pairs set on the next span at start (e.g. gen_ai.input.messages).
|
|
160
157
|
# Consumed when the span is created.
|
|
@@ -169,9 +166,9 @@ module LLM
|
|
|
169
166
|
##
|
|
170
167
|
# Optional: set a proc to supply metadata when the next chat span finishes.
|
|
171
168
|
# The proc is called with the response (res) and should return a Hash of
|
|
172
|
-
# metadata (e.g. { intent: "...", confidence: 1.0 }) to merge onto the span
|
|
173
|
-
#
|
|
174
|
-
#
|
|
169
|
+
# metadata (e.g. { intent: "...", confidence: 1.0 }) to merge onto the span.
|
|
170
|
+
# Cleared after use. Used by apps to attach routing/intent that is only
|
|
171
|
+
# known after the response.
|
|
175
172
|
#
|
|
176
173
|
# @param [Proc, nil] proc (res) -> Hash or nil
|
|
177
174
|
# @return [self]
|
|
@@ -182,19 +179,10 @@ module LLM
|
|
|
182
179
|
|
|
183
180
|
FINISH_METADATA_PROC_KEY = :"llm.tracer.finish_metadata_proc"
|
|
184
181
|
|
|
185
|
-
##
|
|
186
|
-
# Returns the current extra bag (metadata, inputs, outputs) for the current
|
|
187
|
-
# thread/trace. Used by subclasses; default returns empty hashes.
|
|
188
|
-
#
|
|
189
|
-
# @return [Hash] { metadata: {}, inputs: {}, outputs: {} }
|
|
190
|
-
def current_extra
|
|
191
|
-
{}
|
|
192
|
-
end
|
|
193
|
-
|
|
194
182
|
##
|
|
195
183
|
# Returns and clears extra inputs for the next span. Called by the telemetry
|
|
196
|
-
# tracer when starting a span. Subclasses
|
|
197
|
-
#
|
|
184
|
+
# tracer when starting a span. Subclasses can override to return stored
|
|
185
|
+
# inputs; default returns {}.
|
|
198
186
|
#
|
|
199
187
|
# @return [Hash] Attribute key => value to set on the span at start
|
|
200
188
|
def consume_extra_inputs
|
|
@@ -59,9 +59,24 @@ class LLM::Transport
|
|
|
59
59
|
# @return [LLM::Object, String]
|
|
60
60
|
def parse_response(res)
|
|
61
61
|
case res["content-type"]
|
|
62
|
-
when %r{\Aapplication/json\s*}
|
|
62
|
+
when %r{\Aapplication/json\s*}
|
|
63
|
+
body = read_body(res.body)
|
|
64
|
+
LLM::Object.from(LLM.json.load(body))
|
|
63
65
|
else res.body
|
|
64
66
|
end
|
|
65
67
|
end
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# @param [#class] body
|
|
71
|
+
# @return [String]
|
|
72
|
+
def read_body(body)
|
|
73
|
+
case body.class.to_s
|
|
74
|
+
when "Net::ReadAdapter"
|
|
75
|
+
str = +""
|
|
76
|
+
body.read_body { str << _1 }
|
|
77
|
+
str
|
|
78
|
+
else body
|
|
79
|
+
end
|
|
80
|
+
end
|
|
66
81
|
end
|
|
67
82
|
end
|
data/lib/llm/transport/http.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "net/http"
|
|
|
5
5
|
class LLM::Transport
|
|
6
6
|
##
|
|
7
7
|
# The {LLM::Transport::HTTP LLM::Transport::HTTP} transport is the
|
|
8
|
-
# built-in adapter for Ruby's
|
|
8
|
+
# built-in adapter for Ruby's `Net::HTTP`. It manages
|
|
9
9
|
# transient HTTP connections, tracks active requests by owner, and
|
|
10
10
|
# interrupts in-flight requests when needed.
|
|
11
11
|
#
|
|
@@ -69,7 +69,7 @@ class LLM::Transport
|
|
|
69
69
|
|
|
70
70
|
##
|
|
71
71
|
# Performs a request on the current HTTP transport.
|
|
72
|
-
# Accepts both
|
|
72
|
+
# Accepts both `Net::HTTPRequest` and {LLM::Transport::Request}.
|
|
73
73
|
#
|
|
74
74
|
# @param [Net::HTTPRequest, LLM::Transport::Request] request
|
|
75
75
|
# @param [Fiber] owner
|