activeagents-telemetry-ruby_llm 0.2.0 → 0.3.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/README.md +41 -0
- data/lib/activeagents/telemetry/ruby_llm/version.rb +1 -1
- data/lib/activeagents/telemetry/ruby_llm.rb +65 -19
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 255ca4c32e9f775c8b2e45ab431ae81ee410fa3002d66dce026ba06d948da4b6
|
|
4
|
+
data.tar.gz: 1afbdf608524f69045341e10cd7a6f722854cc49977e27ff511cafcc8368fa72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b316442b6a5bb9044df92b691d6d4dee824d12522c45f4367d252cf7041aeee2be2060ce3a604f9b0e36b814c04e78ef5280646ad67ae7f893c29f0a1799c2b
|
|
7
|
+
data.tar.gz: 2fe33cb43e2a35659c2112decb662199b461f756e8ef63560e4e7b63151fde0fdcfa1a4f042c6f0419bead7e41787ec43ba51fdbcee58bc4cd1550014a3f56b5
|
data/README.md
CHANGED
|
@@ -92,3 +92,44 @@ an explicit `flush!`.
|
|
|
92
92
|
```bash
|
|
93
93
|
bundle exec rake test
|
|
94
94
|
```
|
|
95
|
+
|
|
96
|
+
## Correlating evaluation traces
|
|
97
|
+
|
|
98
|
+
Use a separate identity for judge calls and attach stable evaluation identifiers
|
|
99
|
+
without changing the application's default agent resolver:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
trace_ids = []
|
|
103
|
+
ActiveAgents::Telemetry::RubyLLM.with_agent(
|
|
104
|
+
"EvaluationJudge", action: "score",
|
|
105
|
+
attributes: { "eval.run_id" => run_id, "eval.result_id" => result_id },
|
|
106
|
+
on_trace: ->(trace) { trace_ids << trace.trace_id },
|
|
107
|
+
synchronous: true
|
|
108
|
+
) do
|
|
109
|
+
judge_chat.ask(prompt)
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The callback receives each trace the reporter accepted for delivery, so an
|
|
114
|
+
evaluation result can retain the trace ID of that delivery attempt. Acceptance is
|
|
115
|
+
not ingestion: the trace passed the enabled, configured and sampling checks, but
|
|
116
|
+
a delivery that then fails is logged by the reporter rather than announced here,
|
|
117
|
+
and an asynchronous delivery can outlive a process that exits right away. A trace
|
|
118
|
+
dropped by `sample_rate` or by a disabled configuration is never announced.
|
|
119
|
+
`synchronous: true` delivers in the calling thread for this scope only, through
|
|
120
|
+
the same sampling and configuration checks as ordinary delivery; it does not
|
|
121
|
+
mutate the shared async configuration. Normal reporter error logging still
|
|
122
|
+
applies: synchronous delivery does not turn telemetry failures into application
|
|
123
|
+
exceptions. A turn keeps the scope it started under, so a turn left open by a
|
|
124
|
+
pending tool call and closed later by `flush!` still reports with this agent,
|
|
125
|
+
attributes and callback, and a turn that started outside any scope never adopts
|
|
126
|
+
one. Context
|
|
127
|
+
is restored after the block, including when it raises, and nested scopes use
|
|
128
|
+
their own attributes. A callback error is logged by exception class without
|
|
129
|
+
dropping the trace. Attribute redaction and content-capture settings continue to
|
|
130
|
+
apply.
|
|
131
|
+
|
|
132
|
+
Report publication and trace ingestion are separate operations. Persist each run
|
|
133
|
+
and result ID in the evaluation report and attach the same IDs to its response and
|
|
134
|
+
judge trace attributes. The application chooses whether to publish full report
|
|
135
|
+
content; this API does not enable body capture or upload reports automatically.
|
|
@@ -43,7 +43,10 @@ module ActiveAgents
|
|
|
43
43
|
|
|
44
44
|
DEFAULT_AGENT = { name: "RubyLLM::Chat", action: "chat" }.freeze
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
# Span token keys and the reader that answers each on a RubyLLM 2.x +Tokens+.
|
|
47
|
+
TOKEN_READERS = { "input" => :input, "output" => :output, "thinking" => :thinking }.freeze
|
|
48
|
+
|
|
49
|
+
State = Struct.new(:depth, :started_at, :tool_spans, :rounds, :tokens, :chat_key, :agent)
|
|
47
50
|
|
|
48
51
|
class << self
|
|
49
52
|
# Subscribes to RubyLLM's instrumentation.
|
|
@@ -91,17 +94,30 @@ module ActiveAgents
|
|
|
91
94
|
|
|
92
95
|
attr_writer :reporter
|
|
93
96
|
|
|
94
|
-
# Attributes traces inside the block to a named agent/action.
|
|
95
|
-
|
|
97
|
+
# Attributes traces inside the block to a named agent/action. Correlation
|
|
98
|
+
# attributes and the callback apply to this scope only. Short-lived
|
|
99
|
+
# evaluation commands can deliver synchronously without changing the
|
|
100
|
+
# application's shared reporter configuration.
|
|
101
|
+
#
|
|
102
|
+
# A turn keeps the scope it started under, so a turn left open by a
|
|
103
|
+
# pending tool call and closed later by `flush!` still reports as this
|
|
104
|
+
# agent, and a turn that started outside any scope never adopts one.
|
|
105
|
+
# `on_trace` runs only for a trace the reporter accepted, which means
|
|
106
|
+
# it passed the enabled, configured and sampling checks: a delivery
|
|
107
|
+
# that then fails is logged by the reporter, not announced here.
|
|
108
|
+
def with_agent(name, action: "chat", attributes: {}, on_trace: nil, synchronous: false)
|
|
96
109
|
previous = Thread.current[AGENT_KEY]
|
|
97
|
-
Thread.current[AGENT_KEY] = {
|
|
110
|
+
Thread.current[AGENT_KEY] = {
|
|
111
|
+
name: name, action: action, attributes: attributes.to_h.transform_keys(&:to_s),
|
|
112
|
+
on_trace: on_trace, synchronous: synchronous
|
|
113
|
+
}
|
|
98
114
|
yield
|
|
99
115
|
ensure
|
|
100
116
|
Thread.current[AGENT_KEY] = previous
|
|
101
117
|
end
|
|
102
118
|
|
|
103
119
|
def state
|
|
104
|
-
Thread.current[STATE_KEY] ||= State.new(0, nil, [], 0, Span::ZERO_TOKENS.dup, nil)
|
|
120
|
+
Thread.current[STATE_KEY] ||= State.new(0, nil, [], 0, Span::ZERO_TOKENS.dup, nil, nil)
|
|
105
121
|
end
|
|
106
122
|
|
|
107
123
|
def clear_state
|
|
@@ -126,7 +142,13 @@ module ActiveAgents
|
|
|
126
142
|
flush! if turn.rounds.positive? && (turn.chat_key != chat_key || turn_expired?(turn))
|
|
127
143
|
turn = state
|
|
128
144
|
turn.chat_key = chat_key
|
|
129
|
-
turn.started_at
|
|
145
|
+
if turn.started_at.nil?
|
|
146
|
+
turn.started_at = Time.now
|
|
147
|
+
# Captured once, on the turn's first round, whether or not a scope
|
|
148
|
+
# is active: a turn that started unscoped stays unscoped even when
|
|
149
|
+
# a later scope's chat is what flushes it.
|
|
150
|
+
turn.agent = Thread.current[AGENT_KEY]
|
|
151
|
+
end
|
|
130
152
|
end
|
|
131
153
|
turn.depth += 1
|
|
132
154
|
end
|
|
@@ -161,7 +183,7 @@ module ActiveAgents
|
|
|
161
183
|
private
|
|
162
184
|
|
|
163
185
|
def report_turn(payload, turn)
|
|
164
|
-
agent =
|
|
186
|
+
agent = turn.agent || resolve_agent(payload) || DEFAULT_AGENT
|
|
165
187
|
started_at = turn.started_at || Time.now
|
|
166
188
|
finished_at = Time.now
|
|
167
189
|
error = payload[:exception_object]
|
|
@@ -172,12 +194,12 @@ module ActiveAgents
|
|
|
172
194
|
resource_attributes: configuration.resource_attributes
|
|
173
195
|
)
|
|
174
196
|
|
|
175
|
-
root_attributes = {
|
|
197
|
+
root_attributes = (agent[:attributes] || {}).merge(
|
|
176
198
|
"agent.class" => agent[:name],
|
|
177
199
|
"agent.action" => agent[:action],
|
|
178
200
|
"agent.provider" => payload[:provider].to_s,
|
|
179
201
|
"agent.model" => payload[:model].to_s
|
|
180
|
-
|
|
202
|
+
)
|
|
181
203
|
root_attributes.merge!(conversation_attributes(payload)) if configuration.capture_bodies?
|
|
182
204
|
|
|
183
205
|
root = trace.span(
|
|
@@ -206,7 +228,14 @@ module ActiveAgents
|
|
|
206
228
|
trace.add_span(tool_span)
|
|
207
229
|
end
|
|
208
230
|
|
|
209
|
-
reporter.report(trace)
|
|
231
|
+
accepted = agent[:synchronous] ? reporter.report(trace, sync: true) : reporter.report(trace)
|
|
232
|
+
notify_trace(agent[:on_trace], trace) if accepted
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def notify_trace(callback, trace)
|
|
236
|
+
callback&.call(trace)
|
|
237
|
+
rescue StandardError => e
|
|
238
|
+
warn "[#{SDK_NAME}] on_trace failed: #{e.class}"
|
|
210
239
|
end
|
|
211
240
|
|
|
212
241
|
def turn_expired?(turn)
|
|
@@ -280,22 +309,39 @@ module ActiveAgents
|
|
|
280
309
|
text.to_s[0, CONTENT_LIMIT]
|
|
281
310
|
end
|
|
282
311
|
|
|
312
|
+
# Tokens for one round. RubyLLM 2.x puts the round's +Tokens+ on the
|
|
313
|
+
# payload and moves the per-message readers to +message.tokens+; 1.x
|
|
314
|
+
# has +input_tokens+ and friends on each assistant message and nothing
|
|
315
|
+
# on the payload. The payload is read first, then the messages the
|
|
316
|
+
# round added, with either generation's readers.
|
|
283
317
|
def token_totals(payload)
|
|
318
|
+
tokens = payload_tokens(payload[:tokens]) || message_tokens(payload)
|
|
319
|
+
tokens["total"] = tokens.values.sum
|
|
320
|
+
tokens
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def payload_tokens(tokens)
|
|
324
|
+
return unless TOKEN_READERS.values.all? { |reader| tokens.respond_to?(reader) }
|
|
325
|
+
|
|
326
|
+
TOKEN_READERS.to_h { |key, reader| [ key, tokens.public_send(reader).to_i ] }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def message_tokens(payload)
|
|
284
330
|
initial_count = Array(payload[:input_messages]).size
|
|
285
331
|
new_messages = Array(payload[:messages_after])[initial_count..] || []
|
|
286
332
|
assistant_messages = new_messages.select { |message| message.respond_to?(:role) && message.role.to_s == "assistant" }
|
|
287
333
|
|
|
288
|
-
|
|
289
|
-
"input" => sum_tokens(assistant_messages, :input_tokens),
|
|
290
|
-
"output" => sum_tokens(assistant_messages, :output_tokens),
|
|
291
|
-
"thinking" => sum_tokens(assistant_messages, :thinking_tokens)
|
|
292
|
-
}
|
|
293
|
-
tokens["total"] = tokens.values.sum
|
|
294
|
-
tokens
|
|
334
|
+
TOKEN_READERS.to_h { |key, reader| [ key, assistant_messages.sum { |message| message_token_count(message, key, reader) } ] }
|
|
295
335
|
end
|
|
296
336
|
|
|
297
|
-
|
|
298
|
-
|
|
337
|
+
# A 1.x message answers +input_tokens+; a 2.x message answers
|
|
338
|
+
# +tokens.input+.
|
|
339
|
+
def message_token_count(message, key, reader)
|
|
340
|
+
legacy_reader = :"#{key}_tokens"
|
|
341
|
+
return message.public_send(legacy_reader).to_i if message.respond_to?(legacy_reader)
|
|
342
|
+
|
|
343
|
+
tokens = message.tokens if message.respond_to?(:tokens)
|
|
344
|
+
tokens.respond_to?(reader) ? tokens.public_send(reader).to_i : 0
|
|
299
345
|
end
|
|
300
346
|
end
|
|
301
347
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activeagents-telemetry-ruby_llm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ActiveAgents
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activeagents-telemetry
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0.
|
|
19
|
+
version: '0.3'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0.
|
|
26
|
+
version: '0.3'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: activesupport
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|