Skip to content
Closed
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 = {
WorkflowAttributes.WORKFLOW_INPUT: "input",
WorkflowAttributes.WORKFLOW_OUTPUT: "output",
}


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,30 @@ 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 on agent inputs or outputs.

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())
attributes[SpanAttributes.AGENTOPS_SPAN_KIND] = AgentOpsSpanKindValues.GUARDRAIL.value

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

if hasattr(span_data, "triggered") and span_data.triggered is not None:
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 +573,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 @@
return SpanKind.CONSUMER
elif span_type in ["FunctionSpanData", "GenerationSpanData", "ResponseSpanData"]:
return SpanKind.CLIENT
elif span_type in ["HandoffSpanData", "GuardrailSpanData"]:
return SpanKind.INTERNAL

Check warning on line 82 in agentops/instrumentation/agentic/openai_agents/exporter.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/agentic/openai_agents/exporter.py#L81-L82

Added lines #L81 - L82 were not covered by tests
else:
return SpanKind.INTERNAL

Expand Down
6 changes: 2 additions & 4 deletions examples/openai_agents/agent_guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@
input_guardrail,
)

# Initialize agentops and import the guardrail decorator
# Initialize agentops
import agentops
from agentops import guardrail

# Load API keys
import os
Expand All @@ -36,7 +35,7 @@
agentops.init(api_key=os.environ["AGENTOPS_API_KEY"], tags=["agentops-example"])


# OpenAI Agents SDK guardrail example with agentops guardrails decorator for observability
# OpenAI Agents SDK guardrail example with AgentOps observability
class MathHomeworkOutput(BaseModel):
is_math_homework: bool
reasoning: str
Expand All @@ -50,7 +49,6 @@ class MathHomeworkOutput(BaseModel):


@input_guardrail
@guardrail(spec="input") # Specify guardrail type as input or output
async def math_guardrail(
ctx: RunContextWrapper[None], agent: Agent, input: str | list[TResponseInputItem]
) -> GuardrailFunctionOutput:
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,49 @@ 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 = "math_homework_check"
mock_guardrail_span.input = "Can you help me with my math homework?"
mock_guardrail_span.output = "Guardrail triggered: No homework assistance allowed"
mock_guardrail_span.triggered = True

# Extract attributes
attrs = get_guardrail_span_attributes(mock_guardrail_span)

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

def test_guardrail_span_attributes_without_optional_fields(self):
"""Test extraction of attributes from a GuardrailSpanData object without optional fields"""

# Create a simple class instead of MagicMock to avoid automatic attribute creation
class GuardrailSpanData:
def __init__(self):
self.__class__.__name__ = "GuardrailSpanData"
self.input = "Test input"
self.output = "Test output"
# Explicitly no name or triggered attributes

mock_guardrail_span = GuardrailSpanData()

# Extract attributes
attrs = get_guardrail_span_attributes(mock_guardrail_span)

# Verify core attributes are present
assert "agentops.span.kind" in attrs
assert attrs["agentops.span.kind"] == "guardrail"
assert "guardrail.name" not in attrs
assert "guardrail.triggered" not in attrs

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

Expand Down Expand Up @@ -453,13 +497,21 @@ 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.input = "test input"
self.output = "test output"

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 +524,10 @@ def __init__(self):
assert "tool.name" in function_attrs
assert function_attrs["tool.name"] == "test_function"

guardrail_attrs = get_span_attributes(guardrail_span)
assert "agentops.span.kind" in guardrail_attrs
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
Loading
Loading