Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions agentops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@
LLMEvent,
) # type: ignore

# Import all required modules at the top
from opentelemetry.trace import get_current_span
from agentops.semconv import (
AgentAttributes,
ToolAttributes,
WorkflowAttributes,
CoreAttributes,
SpanKind,
SpanAttributes,
)
import json
from typing import List, Optional, Union, Dict, Any
from agentops.client import Client
from agentops.sdk.core import TraceContext, tracer
Expand Down Expand Up @@ -255,13 +266,189 @@
tracer.end_trace(trace_context=trace_context, end_state=end_state)


def update_trace_metadata(metadata: Dict[str, Any], prefix: str = "trace.metadata") -> bool:
"""
Update metadata on the current running trace.

Args:
metadata: Dictionary of key-value pairs to set as trace metadata.
Values must be strings, numbers, booleans, or lists of these types.
Lists are converted to JSON string representation.
Keys can be either custom keys or semantic convention aliases.
prefix: Prefix for metadata attributes (default: "trace.metadata").
Ignored for semantic convention attributes.

Returns:
bool: True if metadata was successfully updated, False otherwise.

"""
if not tracer.initialized:
logger.warning("AgentOps SDK not initialized. Cannot update trace metadata.")
return False

Check warning on line 287 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L285-L287

Added lines #L285 - L287 were not covered by tests

# Build semantic convention mappings dynamically
def build_semconv_mappings():

Check warning on line 290 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L290

Added line #L290 was not covered by tests
"""Build mappings from user-friendly keys to semantic convention attributes."""
mappings = {}

Check warning on line 292 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L292

Added line #L292 was not covered by tests

# Helper function to extract attribute name from semantic convention
def extract_key_from_attr(attr_value: str) -> str:
parts = attr_value.split(".")
if len(parts) >= 2:

Check warning on line 297 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L295-L297

Added lines #L295 - L297 were not covered by tests
# Handle special cases
if parts[0] == "error":

Check warning on line 299 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L299

Added line #L299 was not covered by tests
# error.type -> error_type
return "_".join(parts)

Check warning on line 301 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L301

Added line #L301 was not covered by tests
else:
# Default: entity.attribute -> entity_attribute
return "_".join(parts)
return attr_value

Check warning on line 305 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L304-L305

Added lines #L304 - L305 were not covered by tests

# Process each semantic convention class
for cls in [AgentAttributes, ToolAttributes, WorkflowAttributes, CoreAttributes, SpanAttributes]:
for attr_name, attr_value in cls.__dict__.items():
if not attr_name.startswith("_") and isinstance(attr_value, str):

Check warning on line 310 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L308-L310

Added lines #L308 - L310 were not covered by tests
# Skip gen_ai attributes
if attr_value.startswith("gen_ai."):
continue

Check warning on line 313 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L312-L313

Added lines #L312 - L313 were not covered by tests

# Generate user-friendly key
user_key = extract_key_from_attr(attr_value)
mappings[user_key] = attr_value

Check warning on line 317 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L316-L317

Added lines #L316 - L317 were not covered by tests

# Add some additional convenience mappings
if attr_value == CoreAttributes.TAGS:
mappings["tags"] = attr_value

Check warning on line 321 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L320-L321

Added lines #L320 - L321 were not covered by tests

return mappings

Check warning on line 323 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L323

Added line #L323 was not covered by tests

# Build mappings if using semantic conventions
SEMCONV_MAPPINGS = build_semconv_mappings()

Check warning on line 326 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L326

Added line #L326 was not covered by tests

# Collect all valid semantic convention attributes
VALID_SEMCONV_ATTRS = set()
for cls in [AgentAttributes, ToolAttributes, WorkflowAttributes, CoreAttributes, SpanAttributes]:
for key, value in cls.__dict__.items():
if not key.startswith("_") and isinstance(value, str):

Check warning on line 332 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L329-L332

Added lines #L329 - L332 were not covered by tests
# Include all attributes except gen_ai ones
if not value.startswith("gen_ai."):
VALID_SEMCONV_ATTRS.add(value)

Check warning on line 335 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L334-L335

Added lines #L334 - L335 were not covered by tests

# Find the current trace span
span = None

Check warning on line 338 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L338

Added line #L338 was not covered by tests

# Get the current span from OpenTelemetry context
current_span = get_current_span()

Check warning on line 341 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L341

Added line #L341 was not covered by tests

# Check if the current span is valid and recording
if current_span and hasattr(current_span, "is_recording") and current_span.is_recording():

Check warning on line 344 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L344

Added line #L344 was not covered by tests
# Check if this is a trace/session span or a child span
span_name = getattr(current_span, "name", "")

Check warning on line 346 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L346

Added line #L346 was not covered by tests

# If it's a session/trace span, use it directly
if span_name.endswith(f".{SpanKind.SESSION}"):
span = current_span

Check warning on line 350 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L349-L350

Added lines #L349 - L350 were not covered by tests
else:
# It's a child span, try to find the root trace span
# Get all active traces
active_traces = tracer.get_active_traces()
if active_traces:

Check warning on line 355 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L354-L355

Added lines #L354 - L355 were not covered by tests
# Find the trace that contains the current span
current_trace_id = current_span.get_span_context().trace_id

Check warning on line 357 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L357

Added line #L357 was not covered by tests

for trace_id_str, trace_ctx in active_traces.items():
try:

Check warning on line 360 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L359-L360

Added lines #L359 - L360 were not covered by tests
# Convert hex string back to int for comparison
trace_id = int(trace_id_str, 16)
if trace_id == current_trace_id:
span = trace_ctx.span
break
except (ValueError, AttributeError):
continue

Check warning on line 367 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L362-L367

Added lines #L362 - L367 were not covered by tests

# If we couldn't find the parent trace, use the current span
if not span:
span = current_span

Check warning on line 371 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L370-L371

Added lines #L370 - L371 were not covered by tests
else:
# No active traces, use the current span
span = current_span

Check warning on line 374 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L374

Added line #L374 was not covered by tests

# If no current span or it's not recording, check active traces
if not span:
active_traces = tracer.get_active_traces()
if active_traces:

Check warning on line 379 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L377-L379

Added lines #L377 - L379 were not covered by tests
# Get the most recently created trace (last in the dict)
trace_context = list(active_traces.values())[-1]
span = trace_context.span
logger.debug("Using most recent active trace for metadata update")

Check warning on line 383 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L381-L383

Added lines #L381 - L383 were not covered by tests
else:
logger.warning("No active trace found. Cannot update metadata.")
return False

Check warning on line 386 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L385-L386

Added lines #L385 - L386 were not covered by tests

# Ensure the span is recording before updating
if not span or (hasattr(span, "is_recording") and not span.is_recording()):
logger.warning("Span is not recording. Cannot update metadata.")
return False

Check warning on line 391 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L389-L391

Added lines #L389 - L391 were not covered by tests

# Update the span attributes with the metadata
try:
updated_count = 0
for key, value in metadata.items():

Check warning on line 396 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L394-L396

Added lines #L394 - L396 were not covered by tests
# Validate the value type
if value is None:
continue

Check warning on line 399 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L398-L399

Added lines #L398 - L399 were not covered by tests

# Convert lists to JSON string representation for OpenTelemetry compatibility
if isinstance(value, list):

Check warning on line 402 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L402

Added line #L402 was not covered by tests
# Ensure all list items are valid types
if all(isinstance(item, (str, int, float, bool)) for item in value):
value = json.dumps(value)

Check warning on line 405 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L404-L405

Added lines #L404 - L405 were not covered by tests
else:
logger.warning(f"Skipping metadata key '{key}': list contains invalid types")
continue
elif not isinstance(value, (str, int, float, bool)):
logger.warning(f"Skipping metadata key '{key}': value type {type(value)} not supported")
continue

Check warning on line 411 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L407-L411

Added lines #L407 - L411 were not covered by tests

# Determine the attribute key
attribute_key = key

Check warning on line 414 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L414

Added line #L414 was not covered by tests

# Check if key is already a valid semantic convention attribute
if key in VALID_SEMCONV_ATTRS:

Check warning on line 417 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L417

Added line #L417 was not covered by tests
# Key is already a valid semantic convention, use as-is
attribute_key = key
elif key in SEMCONV_MAPPINGS:

Check warning on line 420 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L419-L420

Added lines #L419 - L420 were not covered by tests
# It's a user-friendly key, map it to semantic convention
attribute_key = SEMCONV_MAPPINGS[key]
logger.debug(f"Mapped '{key}' to semantic convention '{attribute_key}'")

Check warning on line 423 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L422-L423

Added lines #L422 - L423 were not covered by tests
else:
# Not a semantic convention, use with prefix
attribute_key = f"{prefix}.{key}"

Check warning on line 426 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L426

Added line #L426 was not covered by tests

# Set the attribute
span.set_attribute(attribute_key, value)
updated_count += 1

Check warning on line 430 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L429-L430

Added lines #L429 - L430 were not covered by tests

if updated_count > 0:
logger.debug(f"Successfully updated {updated_count} metadata attributes on trace")
return True

Check warning on line 434 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L432-L434

Added lines #L432 - L434 were not covered by tests
else:
logger.warning("No valid metadata attributes were updated")
return False

Check warning on line 437 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L436-L437

Added lines #L436 - L437 were not covered by tests

except Exception as e:
logger.error(f"Error updating trace metadata: {e}")
return False

Check warning on line 441 in agentops/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/__init__.py#L439-L441

Added lines #L439 - L441 were not covered by tests


__all__ = [
"init",
"configure",
"get_client",
"record",
"start_trace",
"end_trace",
"update_trace_metadata",
"start_session",
"end_session",
"track_agent",
Expand Down
17 changes: 17 additions & 0 deletions docs/v2/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,23 @@ def my_workflow_decorated(task_to_perform):
# raise
```

### Updating Trace Metadata

You can also update metadata on running traces to add context or track progress:

```python
from agentops import update_trace_metadata

# Update metadata during trace execution
update_trace_metadata({
"operation_name": "AI Agent Processing",
"processing_stage": "data_validation",
"records_processed": 1500,
"user_id": "user_123",
"tags": ["validation", "production"]
})
```

## Complete Example with Decorators

Here's a consolidated example showcasing how these decorators can work together:
Expand Down
Loading
Loading