Skip to content

Commit 568a23d

Browse files
authored
Merge branch 'main' into langgraph-instrumentation
2 parents 43ed2a4 + b0563b2 commit 568a23d

File tree

6 files changed

+554
-61
lines changed

6 files changed

+554
-61
lines changed

agentops/__init__.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@
1212
LLMEvent,
1313
) # type: ignore
1414

15+
# Import all required modules at the top
16+
from opentelemetry.trace import get_current_span
17+
from agentops.semconv import (
18+
AgentAttributes,
19+
ToolAttributes,
20+
WorkflowAttributes,
21+
CoreAttributes,
22+
SpanKind,
23+
SpanAttributes,
24+
)
25+
import json
1526
from typing import List, Optional, Union, Dict, Any
1627
from agentops.client import Client
1728
from agentops.sdk.core import TraceContext, tracer
@@ -255,13 +266,189 @@ def end_trace(
255266
tracer.end_trace(trace_context=trace_context, end_state=end_state)
256267

257268

269+
def update_trace_metadata(metadata: Dict[str, Any], prefix: str = "trace.metadata") -> bool:
270+
"""
271+
Update metadata on the current running trace.
272+
273+
Args:
274+
metadata: Dictionary of key-value pairs to set as trace metadata.
275+
Values must be strings, numbers, booleans, or lists of these types.
276+
Lists are converted to JSON string representation.
277+
Keys can be either custom keys or semantic convention aliases.
278+
prefix: Prefix for metadata attributes (default: "trace.metadata").
279+
Ignored for semantic convention attributes.
280+
281+
Returns:
282+
bool: True if metadata was successfully updated, False otherwise.
283+
284+
"""
285+
if not tracer.initialized:
286+
logger.warning("AgentOps SDK not initialized. Cannot update trace metadata.")
287+
return False
288+
289+
# Build semantic convention mappings dynamically
290+
def build_semconv_mappings():
291+
"""Build mappings from user-friendly keys to semantic convention attributes."""
292+
mappings = {}
293+
294+
# Helper function to extract attribute name from semantic convention
295+
def extract_key_from_attr(attr_value: str) -> str:
296+
parts = attr_value.split(".")
297+
if len(parts) >= 2:
298+
# Handle special cases
299+
if parts[0] == "error":
300+
# error.type -> error_type
301+
return "_".join(parts)
302+
else:
303+
# Default: entity.attribute -> entity_attribute
304+
return "_".join(parts)
305+
return attr_value
306+
307+
# Process each semantic convention class
308+
for cls in [AgentAttributes, ToolAttributes, WorkflowAttributes, CoreAttributes, SpanAttributes]:
309+
for attr_name, attr_value in cls.__dict__.items():
310+
if not attr_name.startswith("_") and isinstance(attr_value, str):
311+
# Skip gen_ai attributes
312+
if attr_value.startswith("gen_ai."):
313+
continue
314+
315+
# Generate user-friendly key
316+
user_key = extract_key_from_attr(attr_value)
317+
mappings[user_key] = attr_value
318+
319+
# Add some additional convenience mappings
320+
if attr_value == CoreAttributes.TAGS:
321+
mappings["tags"] = attr_value
322+
323+
return mappings
324+
325+
# Build mappings if using semantic conventions
326+
SEMCONV_MAPPINGS = build_semconv_mappings()
327+
328+
# Collect all valid semantic convention attributes
329+
VALID_SEMCONV_ATTRS = set()
330+
for cls in [AgentAttributes, ToolAttributes, WorkflowAttributes, CoreAttributes, SpanAttributes]:
331+
for key, value in cls.__dict__.items():
332+
if not key.startswith("_") and isinstance(value, str):
333+
# Include all attributes except gen_ai ones
334+
if not value.startswith("gen_ai."):
335+
VALID_SEMCONV_ATTRS.add(value)
336+
337+
# Find the current trace span
338+
span = None
339+
340+
# Get the current span from OpenTelemetry context
341+
current_span = get_current_span()
342+
343+
# Check if the current span is valid and recording
344+
if current_span and hasattr(current_span, "is_recording") and current_span.is_recording():
345+
# Check if this is a trace/session span or a child span
346+
span_name = getattr(current_span, "name", "")
347+
348+
# If it's a session/trace span, use it directly
349+
if span_name.endswith(f".{SpanKind.SESSION}"):
350+
span = current_span
351+
else:
352+
# It's a child span, try to find the root trace span
353+
# Get all active traces
354+
active_traces = tracer.get_active_traces()
355+
if active_traces:
356+
# Find the trace that contains the current span
357+
current_trace_id = current_span.get_span_context().trace_id
358+
359+
for trace_id_str, trace_ctx in active_traces.items():
360+
try:
361+
# Convert hex string back to int for comparison
362+
trace_id = int(trace_id_str, 16)
363+
if trace_id == current_trace_id:
364+
span = trace_ctx.span
365+
break
366+
except (ValueError, AttributeError):
367+
continue
368+
369+
# If we couldn't find the parent trace, use the current span
370+
if not span:
371+
span = current_span
372+
else:
373+
# No active traces, use the current span
374+
span = current_span
375+
376+
# If no current span or it's not recording, check active traces
377+
if not span:
378+
active_traces = tracer.get_active_traces()
379+
if active_traces:
380+
# Get the most recently created trace (last in the dict)
381+
trace_context = list(active_traces.values())[-1]
382+
span = trace_context.span
383+
logger.debug("Using most recent active trace for metadata update")
384+
else:
385+
logger.warning("No active trace found. Cannot update metadata.")
386+
return False
387+
388+
# Ensure the span is recording before updating
389+
if not span or (hasattr(span, "is_recording") and not span.is_recording()):
390+
logger.warning("Span is not recording. Cannot update metadata.")
391+
return False
392+
393+
# Update the span attributes with the metadata
394+
try:
395+
updated_count = 0
396+
for key, value in metadata.items():
397+
# Validate the value type
398+
if value is None:
399+
continue
400+
401+
# Convert lists to JSON string representation for OpenTelemetry compatibility
402+
if isinstance(value, list):
403+
# Ensure all list items are valid types
404+
if all(isinstance(item, (str, int, float, bool)) for item in value):
405+
value = json.dumps(value)
406+
else:
407+
logger.warning(f"Skipping metadata key '{key}': list contains invalid types")
408+
continue
409+
elif not isinstance(value, (str, int, float, bool)):
410+
logger.warning(f"Skipping metadata key '{key}': value type {type(value)} not supported")
411+
continue
412+
413+
# Determine the attribute key
414+
attribute_key = key
415+
416+
# Check if key is already a valid semantic convention attribute
417+
if key in VALID_SEMCONV_ATTRS:
418+
# Key is already a valid semantic convention, use as-is
419+
attribute_key = key
420+
elif key in SEMCONV_MAPPINGS:
421+
# It's a user-friendly key, map it to semantic convention
422+
attribute_key = SEMCONV_MAPPINGS[key]
423+
logger.debug(f"Mapped '{key}' to semantic convention '{attribute_key}'")
424+
else:
425+
# Not a semantic convention, use with prefix
426+
attribute_key = f"{prefix}.{key}"
427+
428+
# Set the attribute
429+
span.set_attribute(attribute_key, value)
430+
updated_count += 1
431+
432+
if updated_count > 0:
433+
logger.debug(f"Successfully updated {updated_count} metadata attributes on trace")
434+
return True
435+
else:
436+
logger.warning("No valid metadata attributes were updated")
437+
return False
438+
439+
except Exception as e:
440+
logger.error(f"Error updating trace metadata: {e}")
441+
return False
442+
443+
258444
__all__ = [
259445
"init",
260446
"configure",
261447
"get_client",
262448
"record",
263449
"start_trace",
264450
"end_trace",
451+
"update_trace_metadata",
265452
"start_session",
266453
"end_session",
267454
"track_agent",

0 commit comments

Comments
 (0)