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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '071869beb00d53446a27f489c3817c58b0f8d163f334f0030c95b789a7c4895b'
4
- data.tar.gz: 874227bd4dd05dd2947269bec7da2d593777a8600540ef9ec5a8fb50c346a884
3
+ metadata.gz: 7948199bfccd3d11e42735b9ee9f34a2431d00de5876785fa26e369a128a686c
4
+ data.tar.gz: 7a5a91260825a6c2fc59192dee83e8e050a1260fef3d6a0589a9d1dbd0fbc0fe
5
5
  SHA512:
6
- metadata.gz: 7e3814e0e76359be595534eb92ce11d655e19e3997f78aea006be89721aae771c6a1f97ad277ed80581957cdea5a0919506518dfd8e8de1911a03a3641668a8b
7
- data.tar.gz: 351b99abda0942df5253e1735faca522b5eee988fc248ec16564dea700b2bcfcf31a8ec4ba8f9db73f3f57103e5f8b37a0997c6e71f4b1a1872ca796046cad2a
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, model, message, context_wrapper)
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, model, message, context_wrapper)
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
- input = format_chat_messages(chat)
149
- attrs = {}
150
- attrs[ATTR_LANGFUSE_OBS_INPUT] = input if input
151
- llm_span = @tracer.start_span(@llm_span_name, with_parent: parent_context(tracing), attributes: attrs)
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.set_attribute(ATTR_GEN_AI_REQUEST_MODEL, model) if model
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: attrs)
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 [Proc, nil] Lambda receiving context_wrapper, returning dynamic attributes
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(chat, current_agent.name, current_agent.model, context_wrapper)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Agents
4
- VERSION = "0.11.0"
4
+ VERSION = "0.12.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ai-agents
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shivam Mishra