Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@
}


# Attribute mapping for GuardrailSpanData
GUARDRAIL_SPAN_ATTRIBUTES: AttributeMap = {
"guardrail.name": "name",
"guardrail.triggered": "triggered",
}


def _get_llm_messages_attributes(messages: Optional[List[Dict]], attribute_base: str) -> AttributeMap:
"""
Extracts attributes from a list of message dictionaries (e.g., prompts or completions).
Expand Down Expand Up @@ -512,6 +519,36 @@ def get_speech_group_span_attributes(span_data: Any) -> AttributeMap:
return attributes


def get_guardrail_span_attributes(span_data: Any) -> AttributeMap:
"""Extract attributes from a GuardrailSpanData object.

Guardrails are validation checks that can be triggered during agent execution.
They include a name and a triggered status indicating whether the guardrail
was activated.

Args:
span_data: The GuardrailSpanData object

Returns:
Dictionary of attributes for guardrail span
"""
attributes = _extract_attributes_from_mapping(span_data, GUARDRAIL_SPAN_ATTRIBUTES)
attributes.update(get_common_attributes())

# Set the span kind to guardrail
attributes[SpanAttributes.AGENTOPS_SPAN_KIND] = AgentOpsSpanKindValues.GUARDRAIL.value

# Extract guardrail name directly
if hasattr(span_data, "name") and span_data.name:
attributes["guardrail.name"] = str(span_data.name)

# Extract triggered status
if hasattr(span_data, "triggered"):
attributes["guardrail.triggered"] = bool(span_data.triggered)

return attributes


def get_span_attributes(span_data: Any) -> AttributeMap:
"""Get attributes for a span based on its type.

Expand Down Expand Up @@ -542,6 +579,8 @@ def get_span_attributes(span_data: Any) -> AttributeMap:
attributes = get_speech_span_attributes(span_data)
elif span_type == "SpeechGroupSpanData":
attributes = get_speech_group_span_attributes(span_data)
elif span_type == "GuardrailSpanData":
attributes = get_guardrail_span_attributes(span_data)
else:
logger.debug(f"[agentops.instrumentation.openai_agents.attributes] Unknown span type: {span_type}")
attributes = {}
Expand Down
2 changes: 2 additions & 0 deletions agentops/instrumentation/agentic/openai_agents/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ def get_span_kind(span: Any) -> SpanKind:
return SpanKind.CONSUMER
elif span_type in ["FunctionSpanData", "GenerationSpanData", "ResponseSpanData"]:
return SpanKind.CLIENT
elif span_type == "GuardrailSpanData":
return SpanKind.INTERNAL
else:
return SpanKind.INTERNAL

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
get_agent_span_attributes,
get_function_span_attributes,
get_generation_span_attributes,
get_guardrail_span_attributes,
get_handoff_span_attributes,
get_response_span_attributes,
get_span_attributes,
Expand Down Expand Up @@ -408,6 +409,41 @@ def test_handoff_span_attributes(self):
assert attrs[AgentAttributes.FROM_AGENT] == "source_agent"
assert attrs[AgentAttributes.TO_AGENT] == "target_agent"

def test_guardrail_span_attributes(self):
"""Test extraction of attributes from a GuardrailSpanData object"""
# Create a mock GuardrailSpanData
mock_guardrail_span = MagicMock()
mock_guardrail_span.__class__.__name__ = "GuardrailSpanData"
mock_guardrail_span.name = "content_filter"
mock_guardrail_span.triggered = True

# Extract attributes
attrs = get_guardrail_span_attributes(mock_guardrail_span)

# Verify extracted attributes
assert "guardrail.name" in attrs
assert attrs["guardrail.name"] == "content_filter"
assert "guardrail.triggered" in attrs
assert attrs["guardrail.triggered"] is True
assert "agentops.span.kind" in attrs
assert attrs["agentops.span.kind"] == "guardrail"

def test_guardrail_span_attributes_not_triggered(self):
"""Test extraction of attributes from a GuardrailSpanData object when not triggered"""
# Create a mock GuardrailSpanData with triggered=False
mock_guardrail_span = MagicMock()
mock_guardrail_span.__class__.__name__ = "GuardrailSpanData"
mock_guardrail_span.name = "rate_limiter"
mock_guardrail_span.triggered = False

# Extract attributes
attrs = get_guardrail_span_attributes(mock_guardrail_span)

# Verify extracted attributes
assert attrs["guardrail.name"] == "rate_limiter"
assert attrs["guardrail.triggered"] is False
assert attrs["agentops.span.kind"] == "guardrail"

def test_response_span_attributes(self):
"""Test extraction of attributes from a ResponseSpanData object"""

Expand Down Expand Up @@ -453,13 +489,20 @@ def __init__(self):
self.name = "test_function"
self.input = "test input"

class GuardrailSpanData:
def __init__(self):
self.__class__.__name__ = "GuardrailSpanData"
self.name = "test_guardrail"
self.triggered = True

class UnknownSpanData:
def __init__(self):
self.__class__.__name__ = "UnknownSpanData"

# Use our simple classes
agent_span = AgentSpanData()
function_span = FunctionSpanData()
guardrail_span = GuardrailSpanData()
unknown_span = UnknownSpanData()

# Patch the serialization function to avoid infinite recursion
Expand All @@ -472,6 +515,13 @@ def __init__(self):
assert "tool.name" in function_attrs
assert function_attrs["tool.name"] == "test_function"

# Test dispatcher for guardrail span type
guardrail_attrs = get_span_attributes(guardrail_span)
assert "guardrail.name" in guardrail_attrs
assert guardrail_attrs["guardrail.name"] == "test_guardrail"
assert guardrail_attrs["guardrail.triggered"] is True
assert guardrail_attrs["agentops.span.kind"] == "guardrail"

# Unknown span type should return empty dict
unknown_attrs = get_span_attributes(unknown_span)
assert unknown_attrs == {}
Expand Down