ai-agents 0.11.0 → 0.12.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 +7 -0
- data/lib/agents/instrumentation/constants.rb +4 -0
- data/lib/agents/instrumentation/tracing_callbacks.rb +105 -13
- data/lib/agents/instrumentation.rb +19 -1
- data/lib/agents/runner.rb +4 -2
- data/lib/agents/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: 7948199bfccd3d11e42735b9ee9f34a2431d00de5876785fa26e369a128a686c
|
|
4
|
+
data.tar.gz: 7a5a91260825a6c2fc59192dee83e8e050a1260fef3d6a0589a9d1dbd0fbc0fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d314e6d427f6c9ac5d38ea39b77191c8535e5d0e400690afb15cff48588b7e969b7cb30e88d12e78c027175cdc497e92a74feb899b99f50416af006e3edabe79
|
|
7
|
+
data.tar.gz: 67b9da18847cddfae2b7866f606d335b51690aeb13c9062b8ae2afa8526ed9ae3343fbf86dd435354ee42b6d3145cefcac3e090ee5ca170895cbfc84f3b510b0
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.0] - 2026-06-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Propagate Langfuse trace metadata and filter attributes from root traces to child agent, tool, and generation spans, while keeping span-specific input/output attributes local to each span.
|
|
14
|
+
- Support optional generation-level attributes via `attribute_provider#generation_attributes(context_wrapper, chat, message)`.
|
|
15
|
+
- Add `gen_ai.request.temperature` to generation spans when an agent temperature is configured.
|
|
16
|
+
|
|
10
17
|
## [0.11.0] - 2026-05-27
|
|
11
18
|
|
|
12
19
|
### Added
|
|
@@ -15,6 +15,7 @@ module Agents
|
|
|
15
15
|
|
|
16
16
|
# GenAI semantic conventions (ONLY on generation spans)
|
|
17
17
|
ATTR_GEN_AI_REQUEST_MODEL = "gen_ai.request.model"
|
|
18
|
+
ATTR_GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature"
|
|
18
19
|
ATTR_GEN_AI_PROVIDER = "gen_ai.provider.name"
|
|
19
20
|
ATTR_GEN_AI_USAGE_INPUT = "gen_ai.usage.input_tokens"
|
|
20
21
|
ATTR_GEN_AI_USAGE_OUTPUT = "gen_ai.usage.output_tokens"
|
|
@@ -22,14 +23,17 @@ module Agents
|
|
|
22
23
|
# Langfuse trace-level attributes
|
|
23
24
|
ATTR_LANGFUSE_USER_ID = "langfuse.user.id"
|
|
24
25
|
ATTR_LANGFUSE_SESSION_ID = "langfuse.session.id"
|
|
26
|
+
ATTR_LANGFUSE_PREFIX = "langfuse."
|
|
25
27
|
ATTR_LANGFUSE_TRACE_TAGS = "langfuse.trace.tags"
|
|
26
28
|
ATTR_LANGFUSE_TRACE_INPUT = "langfuse.trace.input"
|
|
27
29
|
ATTR_LANGFUSE_TRACE_OUTPUT = "langfuse.trace.output"
|
|
30
|
+
ATTR_LANGFUSE_TRACE_METADATA_PREFIX = "langfuse.trace.metadata."
|
|
28
31
|
|
|
29
32
|
# Langfuse observation-level attributes
|
|
30
33
|
ATTR_LANGFUSE_OBS_TYPE = "langfuse.observation.type"
|
|
31
34
|
ATTR_LANGFUSE_OBS_INPUT = "langfuse.observation.input"
|
|
32
35
|
ATTR_LANGFUSE_OBS_OUTPUT = "langfuse.observation.output"
|
|
36
|
+
ATTR_LANGFUSE_OBS_METADATA_PREFIX = "langfuse.observation.metadata."
|
|
33
37
|
end
|
|
34
38
|
end
|
|
35
39
|
end
|
|
@@ -18,6 +18,15 @@ module Agents
|
|
|
18
18
|
class TracingCallbacks
|
|
19
19
|
include Constants
|
|
20
20
|
|
|
21
|
+
CHILD_LANGFUSE_EXCLUDED_ATTRIBUTES = [
|
|
22
|
+
ATTR_LANGFUSE_TRACE_INPUT,
|
|
23
|
+
ATTR_LANGFUSE_TRACE_OUTPUT,
|
|
24
|
+
ATTR_LANGFUSE_OBS_INPUT,
|
|
25
|
+
ATTR_LANGFUSE_OBS_OUTPUT,
|
|
26
|
+
ATTR_LANGFUSE_OBS_TYPE
|
|
27
|
+
].freeze
|
|
28
|
+
private_constant :CHILD_LANGFUSE_EXCLUDED_ATTRIBUTES
|
|
29
|
+
|
|
21
30
|
def initialize(tracer:, trace_name: SPAN_RUN, span_attributes: {}, attribute_provider: nil)
|
|
22
31
|
@tracer = tracer
|
|
23
32
|
@trace_name = trace_name
|
|
@@ -31,6 +40,7 @@ module Agents
|
|
|
31
40
|
|
|
32
41
|
def on_run_start(agent_name, input, context_wrapper)
|
|
33
42
|
attributes = build_root_attributes(agent_name, input, context_wrapper)
|
|
43
|
+
child_attributes = build_child_langfuse_attributes(attributes)
|
|
34
44
|
|
|
35
45
|
root_span = @tracer.start_span(@trace_name, attributes: attributes)
|
|
36
46
|
root_context = OpenTelemetry::Trace.context_with_span(root_span)
|
|
@@ -38,6 +48,7 @@ module Agents
|
|
|
38
48
|
store_tracing_state(context_wrapper,
|
|
39
49
|
root_span: root_span,
|
|
40
50
|
root_context: root_context,
|
|
51
|
+
child_langfuse_attributes: child_attributes,
|
|
41
52
|
current_tool_span: nil,
|
|
42
53
|
current_agent_name: nil,
|
|
43
54
|
current_agent_span: nil,
|
|
@@ -66,12 +77,14 @@ module Agents
|
|
|
66
77
|
finish_agent_span(tracing)
|
|
67
78
|
end
|
|
68
79
|
|
|
69
|
-
def on_chat_created(chat, agent_name, model, context_wrapper)
|
|
80
|
+
def on_chat_created(chat, agent_name, model, context_wrapper, temperature = nil)
|
|
70
81
|
tracing = tracing_state(context_wrapper)
|
|
71
82
|
return unless tracing
|
|
72
83
|
|
|
84
|
+
request_attributes = { model: model, temperature: temperature }
|
|
85
|
+
|
|
73
86
|
chat.on_end_message do |message|
|
|
74
|
-
handle_end_message(chat, agent_name,
|
|
87
|
+
handle_end_message(chat, agent_name, request_attributes, message, context_wrapper)
|
|
75
88
|
end
|
|
76
89
|
end
|
|
77
90
|
|
|
@@ -84,6 +97,7 @@ module Agents
|
|
|
84
97
|
ATTR_LANGFUSE_OBS_TYPE => "tool",
|
|
85
98
|
ATTR_LANGFUSE_OBS_INPUT => serialize_output(args)
|
|
86
99
|
}
|
|
100
|
+
attributes.merge!(tracing[:child_langfuse_attributes])
|
|
87
101
|
|
|
88
102
|
parent = handoff_tool?(tool_name) ? tracing[:root_context] : parent_context(tracing)
|
|
89
103
|
tool_span = @tracer.start_span(
|
|
@@ -139,18 +153,19 @@ module Agents
|
|
|
139
153
|
|
|
140
154
|
private
|
|
141
155
|
|
|
142
|
-
def handle_end_message(chat, _agent_name,
|
|
156
|
+
def handle_end_message(chat, _agent_name, request_attributes, message, context_wrapper)
|
|
143
157
|
return unless message.respond_to?(:role) && message.role == :assistant
|
|
144
158
|
|
|
145
159
|
tracing = tracing_state(context_wrapper)
|
|
146
160
|
return unless tracing
|
|
147
161
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
llm_span = @tracer.start_span(
|
|
163
|
+
@llm_span_name,
|
|
164
|
+
with_parent: parent_context(tracing),
|
|
165
|
+
attributes: generation_span_attributes(tracing, chat, message, context_wrapper)
|
|
166
|
+
)
|
|
152
167
|
|
|
153
|
-
llm_span
|
|
168
|
+
set_llm_request_attributes(llm_span, request_attributes)
|
|
154
169
|
|
|
155
170
|
output = llm_output_text(message)
|
|
156
171
|
set_llm_response_attributes(llm_span, message, output)
|
|
@@ -159,6 +174,22 @@ module Agents
|
|
|
159
174
|
llm_span.finish
|
|
160
175
|
end
|
|
161
176
|
|
|
177
|
+
def generation_span_attributes(tracing, chat, message, context_wrapper)
|
|
178
|
+
attrs = tracing[:child_langfuse_attributes].dup
|
|
179
|
+
input = format_chat_messages(chat)
|
|
180
|
+
attrs[ATTR_LANGFUSE_OBS_INPUT] = input if input
|
|
181
|
+
apply_generation_dynamic_attributes(attrs, context_wrapper, chat, message)
|
|
182
|
+
attrs
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def set_llm_request_attributes(span, request_attributes)
|
|
186
|
+
model = request_attributes[:model]
|
|
187
|
+
temperature = request_attributes[:temperature]
|
|
188
|
+
|
|
189
|
+
span.set_attribute(ATTR_GEN_AI_REQUEST_MODEL, model) if model
|
|
190
|
+
span.set_attribute(ATTR_GEN_AI_REQUEST_TEMPERATURE, temperature) unless temperature.nil?
|
|
191
|
+
end
|
|
192
|
+
|
|
162
193
|
def finish_dangling_spans(tracing)
|
|
163
194
|
if tracing[:current_tool_span]
|
|
164
195
|
tracing[:current_tool_span].finish
|
|
@@ -250,13 +281,9 @@ module Agents
|
|
|
250
281
|
finish_agent_span(tracing) # close previous agent span if missed
|
|
251
282
|
|
|
252
283
|
span_name = format(@agent_span_name, agent_name)
|
|
253
|
-
attrs = { "agent.name" => agent_name }
|
|
254
|
-
input = tracing[:pending_llm_input]
|
|
255
|
-
attrs[ATTR_LANGFUSE_OBS_INPUT] = input if input && !input.empty?
|
|
256
|
-
|
|
257
284
|
agent_span = @tracer.start_span(span_name,
|
|
258
285
|
with_parent: tracing[:root_context],
|
|
259
|
-
attributes:
|
|
286
|
+
attributes: agent_span_attributes(tracing, agent_name))
|
|
260
287
|
agent_context = OpenTelemetry::Trace.context_with_span(agent_span)
|
|
261
288
|
|
|
262
289
|
tracing[:current_agent_name] = agent_name
|
|
@@ -265,6 +292,13 @@ module Agents
|
|
|
265
292
|
tracing[:last_agent_output] = nil
|
|
266
293
|
end
|
|
267
294
|
|
|
295
|
+
def agent_span_attributes(tracing, agent_name)
|
|
296
|
+
attrs = tracing[:child_langfuse_attributes].merge("agent.name" => agent_name)
|
|
297
|
+
input = tracing[:pending_llm_input]
|
|
298
|
+
attrs[ATTR_LANGFUSE_OBS_INPUT] = input if input && !input.empty?
|
|
299
|
+
attrs
|
|
300
|
+
end
|
|
301
|
+
|
|
268
302
|
def finish_agent_span(tracing)
|
|
269
303
|
return unless tracing[:current_agent_span]
|
|
270
304
|
|
|
@@ -297,6 +331,57 @@ module Agents
|
|
|
297
331
|
attributes
|
|
298
332
|
end
|
|
299
333
|
|
|
334
|
+
def build_child_langfuse_attributes(root_attributes)
|
|
335
|
+
root_attributes.each_with_object({}) do |(key, value), attrs|
|
|
336
|
+
next if value.nil?
|
|
337
|
+
next unless child_langfuse_attribute?(key)
|
|
338
|
+
|
|
339
|
+
attrs[key] = value
|
|
340
|
+
add_observation_metadata_mirror(attrs, key, value) if mirrored_observation_metadata_attribute?(key)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def child_langfuse_attribute?(key)
|
|
345
|
+
key.start_with?(ATTR_LANGFUSE_PREFIX) && !child_langfuse_excluded_attribute?(key)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def child_langfuse_excluded_attribute?(key)
|
|
349
|
+
CHILD_LANGFUSE_EXCLUDED_ATTRIBUTES.include?(key)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def mirrored_observation_metadata_attribute?(key)
|
|
353
|
+
key == ATTR_LANGFUSE_USER_ID ||
|
|
354
|
+
key == ATTR_LANGFUSE_SESSION_ID ||
|
|
355
|
+
key == ATTR_LANGFUSE_TRACE_TAGS ||
|
|
356
|
+
key.start_with?(ATTR_LANGFUSE_TRACE_METADATA_PREFIX)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def add_observation_metadata_mirror(attrs, key, value)
|
|
360
|
+
metadata_key = observation_metadata_mirror_key(key)
|
|
361
|
+
attrs[observation_metadata_key(metadata_key)] = serialize_metadata_value(value)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def observation_metadata_mirror_key(key)
|
|
365
|
+
case key
|
|
366
|
+
when ATTR_LANGFUSE_USER_ID
|
|
367
|
+
"user_id"
|
|
368
|
+
when ATTR_LANGFUSE_SESSION_ID
|
|
369
|
+
"session_id"
|
|
370
|
+
when ATTR_LANGFUSE_TRACE_TAGS
|
|
371
|
+
"trace_tags"
|
|
372
|
+
else
|
|
373
|
+
key.delete_prefix(ATTR_LANGFUSE_TRACE_METADATA_PREFIX)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def observation_metadata_key(key)
|
|
378
|
+
"#{ATTR_LANGFUSE_OBS_METADATA_PREFIX}#{key}"
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def serialize_metadata_value(value)
|
|
382
|
+
value.is_a?(Hash) || value.is_a?(Array) ? value.to_json : value.to_s
|
|
383
|
+
end
|
|
384
|
+
|
|
300
385
|
def apply_session_id(attributes, context_wrapper)
|
|
301
386
|
session_id = context_wrapper&.context&.dig(:session_id)&.to_s
|
|
302
387
|
attributes[ATTR_LANGFUSE_SESSION_ID] = session_id if session_id && !session_id.empty?
|
|
@@ -317,6 +402,13 @@ module Agents
|
|
|
317
402
|
attributes.merge!(dynamic_attrs) if dynamic_attrs.is_a?(Hash)
|
|
318
403
|
end
|
|
319
404
|
|
|
405
|
+
def apply_generation_dynamic_attributes(attributes, context_wrapper, chat, message)
|
|
406
|
+
return unless @attribute_provider.respond_to?(:generation_attributes)
|
|
407
|
+
|
|
408
|
+
dynamic_attrs = @attribute_provider.generation_attributes(context_wrapper, chat, message)
|
|
409
|
+
attributes.merge!(dynamic_attrs) if dynamic_attrs.is_a?(Hash)
|
|
410
|
+
end
|
|
411
|
+
|
|
320
412
|
def store_tracing_state(context_wrapper, **state)
|
|
321
413
|
context_wrapper.context[:__otel_tracing] = state
|
|
322
414
|
end
|
|
@@ -34,6 +34,22 @@ module Agents
|
|
|
34
34
|
# { 'langfuse.user.id' => ctx.context[:account_id].to_s }
|
|
35
35
|
# }
|
|
36
36
|
# )
|
|
37
|
+
#
|
|
38
|
+
# @example With custom generation attributes
|
|
39
|
+
# class MyAttributeProvider
|
|
40
|
+
# def call(ctx)
|
|
41
|
+
# { 'langfuse.user.id' => ctx.context[:account_id].to_s }
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# def generation_attributes(_ctx, _chat, message)
|
|
45
|
+
# { 'app.generation.has_tool_calls' => message.tool_calls&.any? }
|
|
46
|
+
# end
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# Agents::Instrumentation.install(runner,
|
|
50
|
+
# tracer: tracer,
|
|
51
|
+
# attribute_provider: MyAttributeProvider.new
|
|
52
|
+
# )
|
|
37
53
|
module Instrumentation
|
|
38
54
|
INSTALL_MUTEX = Mutex.new
|
|
39
55
|
private_constant :INSTALL_MUTEX
|
|
@@ -52,7 +68,9 @@ module Agents
|
|
|
52
68
|
# @param tracer [OpenTelemetry::Trace::Tracer] OTel tracer instance
|
|
53
69
|
# @param trace_name [String] Name for the root span (default: "agents.run")
|
|
54
70
|
# @param span_attributes [Hash] Static attributes applied to the root span
|
|
55
|
-
# @param attribute_provider [
|
|
71
|
+
# @param attribute_provider [#call, nil] Object receiving context_wrapper and returning dynamic root attributes.
|
|
72
|
+
# If it also responds to #generation_attributes, that method receives context_wrapper, chat, and message,
|
|
73
|
+
# and can return dynamic attributes for generation spans.
|
|
56
74
|
# @return [Agents::AgentRunner, nil] The runner (for chaining), or nil if OTel is unavailable
|
|
57
75
|
def self.install(runner, tracer:, trace_name: Constants::SPAN_RUN, span_attributes: {},
|
|
58
76
|
attribute_provider: nil)
|
data/lib/agents/runner.rb
CHANGED
|
@@ -115,7 +115,9 @@ module Agents
|
|
|
115
115
|
configure_chat_for_agent(chat, current_agent, context_wrapper, replace: false)
|
|
116
116
|
restore_conversation_history(chat, context_wrapper)
|
|
117
117
|
input_already_in_history = last_message_matches?(chat, input)
|
|
118
|
-
context_wrapper.callback_manager.emit_chat_created(
|
|
118
|
+
context_wrapper.callback_manager.emit_chat_created(
|
|
119
|
+
chat, current_agent.name, current_agent.model, context_wrapper, current_agent.temperature
|
|
120
|
+
)
|
|
119
121
|
|
|
120
122
|
loop do
|
|
121
123
|
current_turn += 1
|
|
@@ -178,7 +180,7 @@ module Agents
|
|
|
178
180
|
current_params = Helpers::HashNormalizer.merge(agent_params, runtime_params)
|
|
179
181
|
apply_params(chat, current_params)
|
|
180
182
|
context_wrapper.callback_manager.emit_chat_created(
|
|
181
|
-
chat, current_agent.name, current_agent.model, context_wrapper
|
|
183
|
+
chat, current_agent.name, current_agent.model, context_wrapper, current_agent.temperature
|
|
182
184
|
)
|
|
183
185
|
|
|
184
186
|
# Force the new agent to respond to the conversation context
|
data/lib/agents/version.rb
CHANGED