From ab293a85110e687e9f7bda87c3bbf08c1d6d46b5 Mon Sep 17 00:00:00 2001 From: wingding12 Date: Tue, 27 Jan 2026 11:13:47 -0500 Subject: [PATCH] fix: add GuardrailSpanData handling for proper span classification Fixes #1105 The OpenAI Agents SDK instrumentation was missing handlers for GuardrailSpanData, causing guardrail events to be incorrectly classified as generic events instead of proper GUARDRAIL span types. Changes: - Add GuardrailSpanData case to get_span_kind() in exporter.py - Add GUARDRAIL_SPAN_ATTRIBUTES mapping in attributes/common.py - Add get_guardrail_span_attributes() function to extract guardrail attributes - Add GuardrailSpanData case to get_span_attributes() dispatcher - Add tests for guardrail span attribute extraction The guardrail spans now properly include: - guardrail.name: The name of the guardrail - guardrail.triggered: Whether the guardrail was activated - agentops.span.kind: Set to "guardrail" for proper classification --- .../openai_agents/attributes/common.py | 39 +++++++++++++++ .../agentic/openai_agents/exporter.py | 2 + .../test_openai_agents_attributes.py | 50 +++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/agentops/instrumentation/agentic/openai_agents/attributes/common.py b/agentops/instrumentation/agentic/openai_agents/attributes/common.py index 154055db1..5c0d74967 100644 --- a/agentops/instrumentation/agentic/openai_agents/attributes/common.py +++ b/agentops/instrumentation/agentic/openai_agents/attributes/common.py @@ -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). @@ -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. @@ -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 = {} diff --git a/agentops/instrumentation/agentic/openai_agents/exporter.py b/agentops/instrumentation/agentic/openai_agents/exporter.py index 1fc5b345a..4068a2b59 100644 --- a/agentops/instrumentation/agentic/openai_agents/exporter.py +++ b/agentops/instrumentation/agentic/openai_agents/exporter.py @@ -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 diff --git a/tests/unit/instrumentation/openai_agents/test_openai_agents_attributes.py b/tests/unit/instrumentation/openai_agents/test_openai_agents_attributes.py index d05f79565..46d0bcd55 100644 --- a/tests/unit/instrumentation/openai_agents/test_openai_agents_attributes.py +++ b/tests/unit/instrumentation/openai_agents/test_openai_agents_attributes.py @@ -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, @@ -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""" @@ -453,6 +489,12 @@ 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" @@ -460,6 +502,7 @@ def __init__(self): # Use our simple classes agent_span = AgentSpanData() function_span = FunctionSpanData() + guardrail_span = GuardrailSpanData() unknown_span = UnknownSpanData() # Patch the serialization function to avoid infinite recursion @@ -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 == {}