llm.rb 16.0.0 → 17.0.1
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 +535 -2
- data/README.md +857 -501
- data/data/alibaba.json +2 -2
- data/data/anthropic.json +66 -3
- data/data/bedrock.json +781 -12
- data/data/deepinfra.json +157 -1
- data/data/google.json +48 -0
- data/data/mistral.json +326 -16
- data/data/moonshot.json +2 -1
- data/data/openai.json +13 -0
- data/data/openrouter.json +1078 -1235
- data/data/xai.json +4 -2
- data/docs/deepdive/advanced/cancellation.md +35 -7
- data/docs/deepdive/advanced/compaction.md +5 -5
- data/docs/deepdive/advanced/context.md +45 -24
- data/docs/deepdive/advanced/{guard.md → intercept.md} +87 -83
- data/docs/deepdive/features/builtin_tools.md +1 -1
- data/docs/deepdive/features/concurrency.md +15 -10
- data/docs/deepdive/features/database.md +19 -19
- data/docs/deepdive/features/embeddings.md +4 -4
- data/docs/deepdive/features/files.md +7 -5
- data/docs/deepdive/fundamentals/agents.md +13 -8
- data/docs/deepdive/fundamentals/providers.md +1 -1
- data/docs/deepdive/fundamentals/schema.md +2 -2
- data/docs/deepdive/fundamentals/stream.md +8 -8
- data/docs/deepdive/fundamentals/tools.md +14 -14
- data/docs/deepdive/media/ocr.md +1 -1
- data/docs/deepdive/protocols/a2a.md +3 -3
- data/docs/deepdive/protocols/mcp.md +116 -7
- data/docs/deepdive/reference/cost.md +1 -1
- data/docs/deepdive/reference/response.md +1 -1
- data/docs/deepdive/reference/tracer.md +90 -22
- data/docs/deepdive.md +1 -1
- data/lib/llm/agent/interrupt.rb +35 -46
- data/lib/llm/agent/registry.rb +106 -0
- data/lib/llm/agent.rb +242 -268
- data/lib/llm/context.rb +63 -55
- data/lib/llm/function/array.rb +2 -1
- data/lib/llm/function/async/group.rb +2 -2
- data/lib/llm/function/async/task.rb +25 -48
- data/lib/llm/function/fiber/group.rb +2 -2
- data/lib/llm/function/fiber/task.rb +16 -30
- data/lib/llm/function/fork/group.rb +2 -2
- data/lib/llm/function/fork/job.rb +8 -4
- data/lib/llm/function/fork/task.rb +25 -30
- data/lib/llm/function/group.rb +33 -5
- data/lib/llm/function/ractor/group.rb +2 -2
- data/lib/llm/function/ractor/job.rb +8 -6
- data/lib/llm/function/ractor/task.rb +32 -9
- data/lib/llm/function/sequential/group.rb +2 -1
- data/lib/llm/function/sequential/task.rb +8 -1
- data/lib/llm/function/task.rb +52 -4
- data/lib/llm/function/thread/group.rb +2 -2
- data/lib/llm/function/thread/task.rb +36 -10
- data/lib/llm/function/window.rb +16 -0
- data/lib/llm/function.rb +14 -43
- data/lib/llm/intercept/multi.rb +40 -0
- data/lib/llm/intercept/null.rb +18 -0
- data/lib/llm/intercept.rb +96 -0
- data/lib/llm/message.rb +1 -1
- data/lib/llm/provider.rb +16 -1
- data/lib/llm/providers/anthropic/stream_parser.rb +1 -1
- data/lib/llm/providers/bedrock/stream_parser.rb +1 -1
- data/lib/llm/providers/deepseek/files.rb +49 -0
- data/lib/llm/providers/deepseek/request_adapter/completion.rb +15 -13
- data/lib/llm/providers/deepseek.rb +16 -2
- data/lib/llm/providers/google/stream_parser.rb +1 -1
- data/lib/llm/providers/openai/responses/stream_parser.rb +1 -1
- data/lib/llm/providers/openai/stream_parser.rb +1 -1
- data/lib/llm/stream.rb +8 -7
- data/lib/llm/tool.rb +1 -1
- data/lib/llm/tools/utils.rb +1 -1
- data/lib/llm/tracer/logger.rb +42 -1
- data/lib/llm/tracer/multi.rb +144 -0
- data/lib/llm/tracer/null.rb +20 -0
- data/lib/llm/tracer/pretty_logger.rb +33 -2
- data/lib/llm/tracer/rescue.rb +110 -0
- data/lib/llm/tracer/telemetry.rb +69 -4
- data/lib/llm/tracer.rb +125 -48
- data/lib/llm/utils.rb +12 -2
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +36 -1
- data/llm.gemspec +9 -4
- metadata +16 -35
- data/lib/llm/function/tracing.rb +0 -21
- data/lib/llm/guard/null.rb +0 -19
- data/lib/llm/guard.rb +0 -55
data/lib/llm/provider.rb
CHANGED
|
@@ -32,7 +32,7 @@ class LLM::Provider
|
|
|
32
32
|
# Requires the net-http-persistent gem.
|
|
33
33
|
# @param [LLM::Transport, Class, nil] transport
|
|
34
34
|
# Optional override with any {LLM::Transport} instance or subclass.
|
|
35
|
-
def initialize(key:, host:, port: 443, timeout:
|
|
35
|
+
def initialize(key:, host:, port: 443, timeout: 240, read_timeout: nil, connect_timeout: 5, ssl: true, base_path: "", persistent: false, transport: nil)
|
|
36
36
|
@key = key
|
|
37
37
|
@host = host
|
|
38
38
|
@port = port
|
|
@@ -482,6 +482,21 @@ class LLM::Provider
|
|
|
482
482
|
raise NotImplementedError
|
|
483
483
|
end
|
|
484
484
|
|
|
485
|
+
##
|
|
486
|
+
# Returns an array of exceptions that a request
|
|
487
|
+
# could raise and that will be retried up to
|
|
488
|
+
# {LLM::Provider#retry_budget} times.
|
|
489
|
+
# @return [Array<Exception>]
|
|
490
|
+
def retry_errors
|
|
491
|
+
[
|
|
492
|
+
LLM::InsufficientQuotaError,
|
|
493
|
+
LLM::RateLimitError,
|
|
494
|
+
Net::ReadTimeout,
|
|
495
|
+
Net::WriteTimeout,
|
|
496
|
+
Net::OpenTimeout
|
|
497
|
+
]
|
|
498
|
+
end
|
|
499
|
+
|
|
485
500
|
private
|
|
486
501
|
|
|
487
502
|
def path(suffix, base_path: true)
|
|
@@ -102,7 +102,7 @@ class LLM::Anthropic
|
|
|
102
102
|
fn.arguments = LLM::Anthropic.parse_tool_input(tool["input"])
|
|
103
103
|
fn.tracer = @stream.extra[:tracer]
|
|
104
104
|
fn.model = @stream.extra[:model]
|
|
105
|
-
fn.
|
|
105
|
+
fn.intercept = @stream.extra[:intercept]
|
|
106
106
|
end
|
|
107
107
|
[fn, (registered ? nil : fn.unavailable)]
|
|
108
108
|
end
|
|
@@ -181,7 +181,7 @@ class LLM::Bedrock
|
|
|
181
181
|
f.arguments = payload["input"] || {}
|
|
182
182
|
f.tracer = @stream.extra[:tracer]
|
|
183
183
|
f.model = @stream.extra[:model]
|
|
184
|
-
f.
|
|
184
|
+
f.intercept = @stream.extra[:intercept]
|
|
185
185
|
end
|
|
186
186
|
[fn, registered ? nil : fn.unavailable]
|
|
187
187
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::DeepSeek
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::DeepSeek::Files LLM::DeepSeek::Files} class provides a files
|
|
6
|
+
# object for interacting with [DeepSeek's Files API](https://api-docs.deepseek.com/guides/files_api).
|
|
7
|
+
# The files API lets a client upload images and reference them later by
|
|
8
|
+
# `file_id` - for example in chat requests to `deepseek-flash` - without
|
|
9
|
+
# re-uploading them. DeepSeek supports the JPEG, PNG, GIF, and WebP formats,
|
|
10
|
+
# and a single file may be at most 64 MiB.
|
|
11
|
+
#
|
|
12
|
+
# DeepSeek's Files API is OpenAI-compatible, with two differences from
|
|
13
|
+
# OpenAI's: it is served from the root of the API host (not under `/v1`),
|
|
14
|
+
# and the `purpose` of an upload must be `"user_data"`.
|
|
15
|
+
#
|
|
16
|
+
# @example example #1
|
|
17
|
+
# #!/usr/bin/env ruby
|
|
18
|
+
# require "llm"
|
|
19
|
+
#
|
|
20
|
+
# llm = LLM.deepseek(key: ENV["KEY"])
|
|
21
|
+
# file = llm.files.create file: "/images/photo.jpg"
|
|
22
|
+
# print "id: ", file.id, "\n"
|
|
23
|
+
class Files < LLM::OpenAI::Files
|
|
24
|
+
##
|
|
25
|
+
# Create a file
|
|
26
|
+
# @example
|
|
27
|
+
# llm = LLM.deepseek(key: ENV["KEY"])
|
|
28
|
+
# res = llm.files.create file: "/images/photo.jpg"
|
|
29
|
+
# @see https://api-docs.deepseek.com/guides/files_api DeepSeek docs
|
|
30
|
+
# @param [File, LLM::File, String] file The file
|
|
31
|
+
# @param [String] purpose The purpose of the file (DeepSeek only supports "user_data")
|
|
32
|
+
# @param [Hash] params Other parameters (see DeepSeek docs)
|
|
33
|
+
# @raise (see LLM::Provider#request)
|
|
34
|
+
# @return [LLM::Response]
|
|
35
|
+
def create(file:, purpose: "user_data", **params)
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# DeepSeek serves its Files API from the root of the API host,
|
|
43
|
+
# while the rest of its OpenAI-compatible API lives under `/v1`
|
|
44
|
+
# @see https://api-docs.deepseek.com/guides/files_api DeepSeek docs
|
|
45
|
+
def path(suffix, **)
|
|
46
|
+
@provider.send(:path, suffix, base_path: false)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -35,7 +35,7 @@ module LLM::DeepSeek::RequestAdapter
|
|
|
35
35
|
when String
|
|
36
36
|
[{type: :text, text: content.to_s}]
|
|
37
37
|
when LLM::Response
|
|
38
|
-
|
|
38
|
+
adapt_remote_file(content)
|
|
39
39
|
when LLM::Message
|
|
40
40
|
adapt_content(content.content)
|
|
41
41
|
when LLM::Function::Return
|
|
@@ -57,7 +57,7 @@ module LLM::DeepSeek::RequestAdapter
|
|
|
57
57
|
when :local_file
|
|
58
58
|
adapt_local_file(object.value)
|
|
59
59
|
when :remote_file
|
|
60
|
-
|
|
60
|
+
adapt_remote_file(object.value)
|
|
61
61
|
else
|
|
62
62
|
prompt_error!(object)
|
|
63
63
|
end
|
|
@@ -80,17 +80,19 @@ module LLM::DeepSeek::RequestAdapter
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
##
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
# @
|
|
87
|
-
#
|
|
88
|
-
# @
|
|
89
|
-
# @
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
# Adapts a file that lives on the provider's side as a `file` content
|
|
84
|
+
# block that references it by `file_id`, which DeepSeek's chat
|
|
85
|
+
# completions API accepts for images uploaded through its Files API
|
|
86
|
+
# @see https://api-docs.deepseek.com/guides/files_api DeepSeek docs
|
|
87
|
+
# @param [LLM::Object, LLM::Response] file The file
|
|
88
|
+
# @return [Array<Hash>]
|
|
89
|
+
# @raise [LLM::PromptError] when the given object is not a file
|
|
90
|
+
def adapt_remote_file(file)
|
|
91
|
+
if file.file?
|
|
92
|
+
[{type: :file, file_id: file.id}]
|
|
93
|
+
else
|
|
94
|
+
prompt_error!(file)
|
|
95
|
+
end
|
|
94
96
|
end
|
|
95
97
|
|
|
96
98
|
def adapt_message
|
|
@@ -20,6 +20,7 @@ module LLM
|
|
|
20
20
|
class DeepSeek < OpenAI
|
|
21
21
|
require_relative "deepseek/request_adapter"
|
|
22
22
|
require_relative "deepseek/response_adapter"
|
|
23
|
+
require_relative "deepseek/files"
|
|
23
24
|
require_relative "deepseek/images"
|
|
24
25
|
include DeepSeek::RequestAdapter
|
|
25
26
|
|
|
@@ -38,9 +39,11 @@ module LLM
|
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
##
|
|
41
|
-
#
|
|
42
|
+
# Provides an interface to DeepSeek's files API
|
|
43
|
+
# @see https://api-docs.deepseek.com/guides/files_api DeepSeek docs
|
|
44
|
+
# @return [LLM::DeepSeek::Files]
|
|
42
45
|
def files
|
|
43
|
-
|
|
46
|
+
LLM::DeepSeek::Files.new(self)
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
##
|
|
@@ -80,5 +83,16 @@ module LLM
|
|
|
80
83
|
def default_model
|
|
81
84
|
"deepseek-flash"
|
|
82
85
|
end
|
|
86
|
+
|
|
87
|
+
##
|
|
88
|
+
# @note
|
|
89
|
+
# This method extends the default errors
|
|
90
|
+
# that are retried to include LLM::ServerError -
|
|
91
|
+
# a common error that DeepSeek returns when
|
|
92
|
+
# it is overloaded.
|
|
93
|
+
# @return [Array<Exception>]
|
|
94
|
+
def retry_errors
|
|
95
|
+
super.concat [LLM::ServerError]
|
|
96
|
+
end
|
|
83
97
|
end
|
|
84
98
|
end
|
|
@@ -150,7 +150,7 @@ class LLM::Google
|
|
|
150
150
|
fn.arguments = call["args"]
|
|
151
151
|
fn.tracer = @stream.extra[:tracer]
|
|
152
152
|
fn.model = @stream.extra[:model]
|
|
153
|
-
fn.
|
|
153
|
+
fn.intercept = @stream.extra[:intercept]
|
|
154
154
|
end
|
|
155
155
|
[fn, (registered ? nil : fn.unavailable)]
|
|
156
156
|
end
|
|
@@ -265,7 +265,7 @@ class LLM::OpenAI
|
|
|
265
265
|
fn.arguments = arguments
|
|
266
266
|
fn.tracer = @stream.extra[:tracer]
|
|
267
267
|
fn.model = @stream.extra[:model]
|
|
268
|
-
fn.
|
|
268
|
+
fn.intercept = @stream.extra[:intercept]
|
|
269
269
|
end
|
|
270
270
|
[fn, (registered ? nil : fn.unavailable)]
|
|
271
271
|
end
|
|
@@ -180,7 +180,7 @@ class LLM::OpenAI
|
|
|
180
180
|
fn.arguments = arguments
|
|
181
181
|
fn.tracer = @stream.extra[:tracer]
|
|
182
182
|
fn.model = @stream.extra[:model]
|
|
183
|
-
fn.
|
|
183
|
+
fn.intercept = @stream.extra[:intercept]
|
|
184
184
|
end
|
|
185
185
|
[fn, (registered ? nil : fn.unavailable)]
|
|
186
186
|
end
|
data/lib/llm/stream.rb
CHANGED
|
@@ -132,14 +132,15 @@ module LLM
|
|
|
132
132
|
end
|
|
133
133
|
|
|
134
134
|
##
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
135
|
+
# This method is called when the model
|
|
136
|
+
# requests a tool call. A little known
|
|
137
|
+
# feature: it is possible to spawn a tool
|
|
138
|
+
# call during a stream through:
|
|
139
|
+
#
|
|
140
|
+
# @queue << tool.task(:thread).tap(&:spawn)
|
|
141
|
+
#
|
|
141
142
|
# @param [LLM::Function] tool
|
|
142
|
-
#
|
|
143
|
+
# An instance of {LLM::Function LLM::Function}
|
|
143
144
|
# @return [nil]
|
|
144
145
|
def on_tool_call(tool)
|
|
145
146
|
nil
|
data/lib/llm/tool.rb
CHANGED
|
@@ -205,7 +205,7 @@ class LLM::Tool
|
|
|
205
205
|
# @return [Class<LLM::Tool>]
|
|
206
206
|
# @raise [LLM::NoSuchToolError]
|
|
207
207
|
def self.find_by_name!(name)
|
|
208
|
-
find_by_name(name) || raise(LLM::NoSuchToolError, "
|
|
208
|
+
find_by_name(name) || raise(LLM::NoSuchToolError, "tool '#{name}' not found")
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
##
|
data/lib/llm/tools/utils.rb
CHANGED
data/lib/llm/tracer/logger.rb
CHANGED
|
@@ -56,8 +56,12 @@ module LLM
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
##
|
|
59
|
+
# @note An "ending" describes the method
|
|
60
|
+
# that matches the end of a tool call, and it can be one of:
|
|
61
|
+
# on_tool_finish, on_tool_error or on_tool_interrupt.
|
|
59
62
|
# @param (see LLM::Tracer#on_tool_start)
|
|
60
|
-
# @return [
|
|
63
|
+
# @return [LLM::Object]
|
|
64
|
+
# The span an ending is handed, with the call's id and name
|
|
61
65
|
def on_tool_start(id:, name:, arguments:, model:, **)
|
|
62
66
|
@logger.info(
|
|
63
67
|
tracer: "llm.rb (logger)",
|
|
@@ -69,6 +73,7 @@ module LLM
|
|
|
69
73
|
tool_arguments: arguments,
|
|
70
74
|
model:
|
|
71
75
|
)
|
|
76
|
+
LLM::Object.from(id:, name:)
|
|
72
77
|
end
|
|
73
78
|
|
|
74
79
|
##
|
|
@@ -95,11 +100,47 @@ module LLM
|
|
|
95
100
|
event: "tool.error",
|
|
96
101
|
provider: provider_name,
|
|
97
102
|
operation: "execute_tool",
|
|
103
|
+
error_class: ex.value[:type],
|
|
104
|
+
error_message: ex.value[:message]
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# @param (see LLM::Tracer#on_tool_interrupt)
|
|
110
|
+
# @return [void]
|
|
111
|
+
def on_tool_interrupt(ex:, span:, **)
|
|
112
|
+
@logger.info(
|
|
113
|
+
tracer: "llm.rb (logger)",
|
|
114
|
+
event: "tool.interrupt",
|
|
115
|
+
provider: provider_name,
|
|
116
|
+
operation: "execute_tool",
|
|
117
|
+
tool_id: span.id,
|
|
118
|
+
tool_name: span.name,
|
|
98
119
|
error_class: ex.class.to_s,
|
|
99
120
|
error_message: ex.message
|
|
100
121
|
)
|
|
101
122
|
end
|
|
102
123
|
|
|
124
|
+
##
|
|
125
|
+
# @param (see LLM::Tracer#on_interrupt)
|
|
126
|
+
# @return [void]
|
|
127
|
+
def on_interrupt(scope:, request_id: nil, **)
|
|
128
|
+
@logger.info(
|
|
129
|
+
tracer: "llm.rb (logger)",
|
|
130
|
+
event: "#{scope}.interrupt",
|
|
131
|
+
provider: provider_name,
|
|
132
|
+
scope:,
|
|
133
|
+
request_id:
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# No-op.
|
|
139
|
+
# @return [nil]
|
|
140
|
+
def on_exit
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
|
|
103
144
|
private
|
|
104
145
|
|
|
105
146
|
##
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Tracer
|
|
4
|
+
##
|
|
5
|
+
# A tracer that fans out every tracing callback to a set of
|
|
6
|
+
# tracers, so a request can (for example) be logged to a file
|
|
7
|
+
# and exported to OpenTelemetry at the same time.
|
|
8
|
+
#
|
|
9
|
+
# {on_request_start} and {on_tool_start} return an array with
|
|
10
|
+
# one span per tracer, in the same order as {#tracers}. Pass the
|
|
11
|
+
# array back as `span:` to the matching finish, error, or
|
|
12
|
+
# interrupt callback, and each tracer is handed back the span
|
|
13
|
+
# it returned.
|
|
14
|
+
#
|
|
15
|
+
# A tracer that raises does not take the others down with it:
|
|
16
|
+
# the error is reported the same way
|
|
17
|
+
# {LLM::Tracer::Rescue LLM::Tracer::Rescue} reports a crashed
|
|
18
|
+
# tracer, and the remaining tracers still run.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# LLM::Tracer::Multi.new(
|
|
22
|
+
# LLM::Tracer.logger(llm, io: $stdout),
|
|
23
|
+
# LLM::Tracer.telemetry(llm)
|
|
24
|
+
# )
|
|
25
|
+
class Multi < self
|
|
26
|
+
##
|
|
27
|
+
# Returns the tracer(s) under the supervision of Multi
|
|
28
|
+
# @return [Array<LLM::Tracer>]
|
|
29
|
+
attr_reader :tracers
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# @param [Array<LLM::Tracer>] tracers
|
|
33
|
+
# The tracers to fan out to. A Multi has no provider
|
|
34
|
+
# of its own.
|
|
35
|
+
def initialize(*tracers)
|
|
36
|
+
super(nil)
|
|
37
|
+
@tracers = tracers
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# @param (see LLM::Tracer#on_request_start)
|
|
42
|
+
# @return [Array]
|
|
43
|
+
# One span per tracer, in the same order as {#tracers}
|
|
44
|
+
def on_request_start(operation:, request_id:, model: nil, inputs: nil)
|
|
45
|
+
@tracers.map { _1.on_request_start(operation:, request_id:, model:, inputs:) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# @param (see LLM::Tracer#on_request_finish)
|
|
50
|
+
# @return [void]
|
|
51
|
+
def on_request_finish(operation:, res:, model: nil, span: nil, outputs: nil, metadata: nil, request_id:)
|
|
52
|
+
span.each.with_index do |s, i|
|
|
53
|
+
@tracers[i].on_request_finish(operation:, res:, model:, span: s, outputs:, metadata:, request_id:)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# @param (see LLM::Tracer#on_request_error)
|
|
59
|
+
# @return [void]
|
|
60
|
+
def on_request_error(ex:, span:, request_id:)
|
|
61
|
+
span.each.with_index do |s, i|
|
|
62
|
+
@tracers[i].on_request_error(ex:, span: s, request_id:)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# @param (see LLM::Tracer#on_tool_start)
|
|
68
|
+
# @return [Array]
|
|
69
|
+
# One span per tracer, in the same order as {#tracers}
|
|
70
|
+
def on_tool_start(id:, name:, arguments:, model:)
|
|
71
|
+
@tracers.map { _1.on_tool_start(id:, name:, arguments:, model:) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
##
|
|
75
|
+
# @param (see LLM::Tracer#on_tool_interrupt)
|
|
76
|
+
# @return [void]
|
|
77
|
+
def on_tool_interrupt(ex:, span:, started_at:, finished_at:)
|
|
78
|
+
span.each.with_index do |s, i|
|
|
79
|
+
@tracers[i].on_tool_interrupt(ex:, span: s, started_at:, finished_at:)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# @param (see LLM::Tracer#on_tool_finish)
|
|
85
|
+
# @return [void]
|
|
86
|
+
def on_tool_finish(result:, span:, started_at:, finished_at:)
|
|
87
|
+
span.each.with_index do |s, i|
|
|
88
|
+
@tracers[i].on_tool_finish(result:, span: s, started_at:, finished_at:)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# @param (see LLM::Tracer#on_tool_error)
|
|
94
|
+
# @return [void]
|
|
95
|
+
def on_tool_error(ex:, span:, started_at:, finished_at:)
|
|
96
|
+
span.each.with_index do |s, i|
|
|
97
|
+
@tracers[i].on_tool_error(ex:, span: s, started_at:, finished_at:)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
##
|
|
102
|
+
# @param (see LLM::Tracer#on_interrupt)
|
|
103
|
+
# @return [void]
|
|
104
|
+
def on_interrupt(scope:, span: nil, request_id: nil)
|
|
105
|
+
spans = span || Array.new(@tracers.size)
|
|
106
|
+
spans.each.with_index do |s, i|
|
|
107
|
+
@tracers[i].on_interrupt(scope:, span: s, request_id:)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
##
|
|
112
|
+
# @return [void]
|
|
113
|
+
def on_exit
|
|
114
|
+
@tracers.each(&:on_exit)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
##
|
|
118
|
+
# @param (see LLM::Tracer#start_trace)
|
|
119
|
+
# @return [Array<LLM::Tracer>]
|
|
120
|
+
def start_trace(...)
|
|
121
|
+
@tracers.each { _1.start_trace(...) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
##
|
|
125
|
+
# @return [Array<LLM::Tracer>]
|
|
126
|
+
def stop_trace
|
|
127
|
+
@tracers.each(&:stop_trace)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
##
|
|
131
|
+
# @param (see LLM::Tracer#set_finish_metadata_proc)
|
|
132
|
+
# @return [Array<LLM::Tracer>]
|
|
133
|
+
def set_finish_metadata_proc(...)
|
|
134
|
+
@tracers.each { _1.set_finish_metadata_proc(...) }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# @param (see LLM::Tracer#merge_extra)
|
|
139
|
+
# @return [Array<LLM::Tracer>]
|
|
140
|
+
def merge_extra(...)
|
|
141
|
+
@tracers.each { _1.merge_extra(...) }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
data/lib/llm/tracer/null.rb
CHANGED
|
@@ -32,6 +32,13 @@ module LLM
|
|
|
32
32
|
nil
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
##
|
|
36
|
+
# @param (see LLM::Tracer#on_tool_interrupt)
|
|
37
|
+
# @return [nil]
|
|
38
|
+
def on_tool_interrupt(**)
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
35
42
|
##
|
|
36
43
|
# @param (see LLM::Tracer#on_tool_finish)
|
|
37
44
|
# @return [nil]
|
|
@@ -46,6 +53,19 @@ module LLM
|
|
|
46
53
|
nil
|
|
47
54
|
end
|
|
48
55
|
|
|
56
|
+
##
|
|
57
|
+
# @param (see LLM::Tracer#on_interrupt)
|
|
58
|
+
# @return [nil]
|
|
59
|
+
def on_interrupt(**)
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# @return [nil]
|
|
65
|
+
def on_exit
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
49
69
|
##
|
|
50
70
|
# @param (see LLM::Tracer#set_finish_metadata_proc)
|
|
51
71
|
# @return [self]
|
|
@@ -52,9 +52,11 @@ module LLM
|
|
|
52
52
|
|
|
53
53
|
##
|
|
54
54
|
# @param (see LLM::Tracer#on_tool_start)
|
|
55
|
-
# @return [
|
|
55
|
+
# @return [LLM::Object]
|
|
56
|
+
# The span an ending is handed, with the call's id and name
|
|
56
57
|
def on_tool_start(id:, name:, arguments:, **)
|
|
57
58
|
@io.puts "#{timestamp} #{name}(#{format_arguments(arguments)})"
|
|
59
|
+
LLM::Object.from(id:, name:)
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
##
|
|
@@ -68,7 +70,28 @@ module LLM
|
|
|
68
70
|
# @param (see LLM::Tracer#on_tool_error)
|
|
69
71
|
# @return [void]
|
|
70
72
|
def on_tool_error(ex:, **)
|
|
71
|
-
@io.puts "#{timestamp} tool error #{ex.
|
|
73
|
+
@io.puts "#{timestamp} tool error #{ex.value[:type]}: #{ex.value[:message]}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# @param (see LLM::Tracer#on_tool_interrupt)
|
|
78
|
+
# @return [void]
|
|
79
|
+
def on_tool_interrupt(ex:, span:, **)
|
|
80
|
+
@io.puts "#{timestamp} tool #{span.name} (#{format_id(span.id)}) received an interrupt"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# @param (see LLM::Tracer#on_interrupt)
|
|
85
|
+
# @return [void]
|
|
86
|
+
def on_interrupt(scope:, **)
|
|
87
|
+
@io.puts "#{timestamp} #{provider_name} interrupt received (scope=#{scope})"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# No-op.
|
|
92
|
+
# @return [nil]
|
|
93
|
+
def on_exit
|
|
94
|
+
nil
|
|
72
95
|
end
|
|
73
96
|
|
|
74
97
|
private
|
|
@@ -123,5 +146,13 @@ module LLM
|
|
|
123
146
|
str.size > max ? "#{str[0...max]}..." : str
|
|
124
147
|
end
|
|
125
148
|
end
|
|
149
|
+
|
|
150
|
+
##
|
|
151
|
+
# A call id, which is a long string that reads as gibberish:
|
|
152
|
+
# ten characters are enough to tell two of them apart.
|
|
153
|
+
def format_id(id)
|
|
154
|
+
id = id.to_s
|
|
155
|
+
id.size > 10 ? "#{id[0, 10]}..." : id
|
|
156
|
+
end
|
|
126
157
|
end
|
|
127
158
|
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Tracer
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Tracer::Rescue} module prepends
|
|
6
|
+
# itself to every subclass of {LLM::Tracer}
|
|
7
|
+
# and ensures that a tracer callback never
|
|
8
|
+
# crashes an agent.
|
|
9
|
+
#
|
|
10
|
+
# When an error occurs, it is dumped to standard
|
|
11
|
+
# error and does not travel up the stack. But
|
|
12
|
+
# LLM::Interrupt, and any other exception not
|
|
13
|
+
# covered by the 'ERRORS' constant can crash
|
|
14
|
+
# the tracer. So it handles most cases but not
|
|
15
|
+
# all, and LLM::Interrupt travelling up the stack
|
|
16
|
+
# is intentional.
|
|
17
|
+
module Rescue
|
|
18
|
+
##
|
|
19
|
+
# @param [StandardError] ex
|
|
20
|
+
# @return [void]
|
|
21
|
+
def self.crash(tracer, ex)
|
|
22
|
+
$stderr.puts "",
|
|
23
|
+
"an llm.rb tracer has crashed.",
|
|
24
|
+
"",
|
|
25
|
+
"[ tracer ] #{tracer.class}",
|
|
26
|
+
"[ class ] #{ex.class}",
|
|
27
|
+
"[ message ] #{ex.message}",
|
|
28
|
+
"[ backtrace ] ",
|
|
29
|
+
"#{(ex.backtrace || []).take(10).join("\n")}",
|
|
30
|
+
""
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# @api private
|
|
35
|
+
ERRORS = [StandardError, ScriptError]
|
|
36
|
+
private_constant :ERRORS
|
|
37
|
+
|
|
38
|
+
def on_exit
|
|
39
|
+
super
|
|
40
|
+
rescue LLM::Interrupt
|
|
41
|
+
raise
|
|
42
|
+
rescue *ERRORS => ex
|
|
43
|
+
LLM::Tracer::Rescue.crash(self, ex)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def on_request_start(...)
|
|
47
|
+
super
|
|
48
|
+
rescue LLM::Interrupt
|
|
49
|
+
raise
|
|
50
|
+
rescue *ERRORS => ex
|
|
51
|
+
LLM::Tracer::Rescue.crash(self, ex)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def on_request_finish(...)
|
|
55
|
+
super
|
|
56
|
+
rescue LLM::Interrupt
|
|
57
|
+
raise
|
|
58
|
+
rescue *ERRORS => ex
|
|
59
|
+
LLM::Tracer::Rescue.crash(self, ex)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def on_request_error(...)
|
|
63
|
+
super
|
|
64
|
+
rescue LLM::Interrupt
|
|
65
|
+
raise
|
|
66
|
+
rescue *ERRORS => ex
|
|
67
|
+
LLM::Tracer::Rescue.crash(self, ex)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def on_tool_start(...)
|
|
71
|
+
super
|
|
72
|
+
rescue LLM::Interrupt
|
|
73
|
+
raise
|
|
74
|
+
rescue *ERRORS => ex
|
|
75
|
+
LLM::Tracer::Rescue.crash(self, ex)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def on_tool_interrupt(...)
|
|
79
|
+
super
|
|
80
|
+
rescue LLM::Interrupt
|
|
81
|
+
raise
|
|
82
|
+
rescue *ERRORS => ex
|
|
83
|
+
LLM::Tracer::Rescue.crash(self, ex)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def on_tool_finish(...)
|
|
87
|
+
super
|
|
88
|
+
rescue LLM::Interrupt
|
|
89
|
+
raise
|
|
90
|
+
rescue *ERRORS => ex
|
|
91
|
+
LLM::Tracer::Rescue.crash(self, ex)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def on_tool_error(...)
|
|
95
|
+
super
|
|
96
|
+
rescue LLM::Interrupt
|
|
97
|
+
raise
|
|
98
|
+
rescue *ERRORS => ex
|
|
99
|
+
LLM::Tracer::Rescue.crash(self, ex)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def on_interrupt(...)
|
|
103
|
+
super
|
|
104
|
+
rescue LLM::Interrupt
|
|
105
|
+
raise
|
|
106
|
+
rescue *ERRORS => ex
|
|
107
|
+
LLM::Tracer::Rescue.crash(self, ex)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|