|
12 | 12 | LLMEvent, |
13 | 13 | ) # type: ignore |
14 | 14 |
|
| 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 |
15 | 26 | from typing import List, Optional, Union, Dict, Any |
16 | 27 | from agentops.client import Client |
17 | 28 | from agentops.sdk.core import TraceContext, tracer |
@@ -255,13 +266,189 @@ def end_trace( |
255 | 266 | tracer.end_trace(trace_context=trace_context, end_state=end_state) |
256 | 267 |
|
257 | 268 |
|
| 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 | + |
258 | 444 | __all__ = [ |
259 | 445 | "init", |
260 | 446 | "configure", |
261 | 447 | "get_client", |
262 | 448 | "record", |
263 | 449 | "start_trace", |
264 | 450 | "end_trace", |
| 451 | + "update_trace_metadata", |
265 | 452 | "start_session", |
266 | 453 | "end_session", |
267 | 454 | "track_agent", |
|
0 commit comments