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
26 changes: 26 additions & 0 deletions src/microsoft/opentelemetry/a365/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
EXECUTE_TOOL_OPERATION_NAME = "execute_tool"
OUTPUT_MESSAGES_OPERATION_NAME = "output_messages"
CHAT_OPERATION_NAME = "chat"
APPLY_GUARDRAIL_OPERATION_NAME = "apply_guardrail"

# --- OpenTelemetry semantic conventions ---
ERROR_TYPE_KEY = "error.type"
Expand Down Expand Up @@ -109,6 +110,31 @@
CHANNEL_NAME_KEY = "microsoft.channel.name"
CHANNEL_LINK_KEY = "microsoft.channel.link"

# --- Guardrail / Security ---
GEN_AI_GUARDIAN_ID_KEY = "microsoft.guardian.id"
GEN_AI_GUARDIAN_NAME_KEY = "microsoft.guardian.name"
GEN_AI_GUARDIAN_PROVIDER_NAME_KEY = "microsoft.guardian.provider.name"
GEN_AI_GUARDIAN_VERSION_KEY = "microsoft.guardian.version"
GEN_AI_SECURITY_DECISION_TYPE_KEY = "microsoft.security.decision.type"
GEN_AI_SECURITY_DECISION_REASON_KEY = "microsoft.security.decision.reason"
GEN_AI_SECURITY_DECISION_CODE_KEY = "microsoft.security.decision.code"
GEN_AI_SECURITY_TARGET_TYPE_KEY = "microsoft.security.target.type"
GEN_AI_SECURITY_TARGET_ID_KEY = "microsoft.security.target.id"
GEN_AI_SECURITY_POLICY_ID_KEY = "microsoft.security.policy.id"
GEN_AI_SECURITY_POLICY_NAME_KEY = "microsoft.security.policy.name"
GEN_AI_SECURITY_POLICY_VERSION_KEY = "microsoft.security.policy.version"
GEN_AI_SECURITY_CONTENT_INPUT_HASH_KEY = "microsoft.security.content.input.hash"
GEN_AI_SECURITY_CONTENT_MODIFIED_KEY = "microsoft.security.content.modified"
GEN_AI_SECURITY_EXTERNAL_EVENT_ID_KEY = "microsoft.security.external_event_id"
GEN_AI_SECURITY_CONTENT_INPUT_VALUE_KEY = "microsoft.security.content.input.value"
GEN_AI_SECURITY_CONTENT_OUTPUT_VALUE_KEY = "microsoft.security.content.output.value"
GEN_AI_SECURITY_FINDING_EVENT_NAME = "microsoft.security.finding"
GEN_AI_SECURITY_RISK_CATEGORY_KEY = "microsoft.security.risk.category"
GEN_AI_SECURITY_RISK_SEVERITY_KEY = "microsoft.security.risk.severity"
GEN_AI_SECURITY_RISK_SCORE_KEY = "microsoft.security.risk.score"
GEN_AI_SECURITY_RISK_METADATA_KEY = "microsoft.security.risk.metadata"
GEN_AI_SECURITY_POLICY_DECISION_TYPE_KEY = "microsoft.security.policy.decision.type"

# --- Telemetry SDK attributes ---
TELEMETRY_SDK_NAME_KEY = "telemetry.sdk.name"
TELEMETRY_SDK_LANGUAGE_KEY = "telemetry.sdk.language"
Expand Down
13 changes: 13 additions & 0 deletions src/microsoft/opentelemetry/a365/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
# Microsoft Agent 365 Python SDK for OpenTelemetry tracing.

from microsoft.opentelemetry.a365.core.agent_details import AgentDetails
from microsoft.opentelemetry.a365.core.apply_guardrail_scope import ApplyGuardrailScope
from microsoft.opentelemetry.a365.core.execute_tool_scope import ExecuteToolScope
from microsoft.opentelemetry.a365.core.guardrail_decision_type import GuardrailDecisionType
from microsoft.opentelemetry.a365.core.guardrail_details import GuardrailDetails
from microsoft.opentelemetry.a365.core.guardrail_finding import GuardrailFinding
from microsoft.opentelemetry.a365.core.guardrail_risk_severity import GuardrailRiskSeverity
from microsoft.opentelemetry.a365.core.guardrail_target_type import GuardrailTargetType
from microsoft.opentelemetry.a365.core.inference_call_details import InferenceCallDetails
from microsoft.opentelemetry.a365.core.models.service_endpoint import ServiceEndpoint
from microsoft.opentelemetry.a365.core.inference_operation_type import InferenceOperationType
Expand Down Expand Up @@ -49,10 +55,17 @@
# Base scope class
"OpenTelemetryScope",
# Specific scope classes
"ApplyGuardrailScope",
"ExecuteToolScope",
"InvokeAgentScope",
"InferenceScope",
"OutputScope",
# Guardrail data classes and constants
"GuardrailDecisionType",
"GuardrailDetails",
"GuardrailFinding",
"GuardrailRiskSeverity",
"GuardrailTargetType",
# Middleware
"BaggageBuilder",
# Data classes
Expand Down
260 changes: 260 additions & 0 deletions src/microsoft/opentelemetry/a365/core/apply_guardrail_scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""OpenTelemetry tracing scope for guardrail (security guardian) evaluations."""

from __future__ import annotations

from opentelemetry.trace import SpanKind

from microsoft.opentelemetry.a365.core.agent_details import AgentDetails
from microsoft.opentelemetry.a365.core.constants import (
APPLY_GUARDRAIL_OPERATION_NAME,
CHANNEL_LINK_KEY,
CHANNEL_NAME_KEY,
GEN_AI_CALLER_CLIENT_IP_KEY,
GEN_AI_CONVERSATION_ID_KEY,
GEN_AI_GUARDIAN_ID_KEY,
GEN_AI_GUARDIAN_NAME_KEY,
GEN_AI_GUARDIAN_PROVIDER_NAME_KEY,
GEN_AI_GUARDIAN_VERSION_KEY,
GEN_AI_SECURITY_CONTENT_INPUT_HASH_KEY,
GEN_AI_SECURITY_CONTENT_INPUT_VALUE_KEY,
GEN_AI_SECURITY_CONTENT_MODIFIED_KEY,
GEN_AI_SECURITY_CONTENT_OUTPUT_VALUE_KEY,
GEN_AI_SECURITY_DECISION_CODE_KEY,
GEN_AI_SECURITY_DECISION_REASON_KEY,
GEN_AI_SECURITY_DECISION_TYPE_KEY,
GEN_AI_SECURITY_EXTERNAL_EVENT_ID_KEY,
GEN_AI_SECURITY_FINDING_EVENT_NAME,
GEN_AI_SECURITY_POLICY_DECISION_TYPE_KEY,
GEN_AI_SECURITY_POLICY_ID_KEY,
GEN_AI_SECURITY_POLICY_NAME_KEY,
GEN_AI_SECURITY_POLICY_VERSION_KEY,
GEN_AI_SECURITY_RISK_CATEGORY_KEY,
GEN_AI_SECURITY_RISK_METADATA_KEY,
GEN_AI_SECURITY_RISK_SCORE_KEY,
GEN_AI_SECURITY_RISK_SEVERITY_KEY,
GEN_AI_SECURITY_TARGET_ID_KEY,
GEN_AI_SECURITY_TARGET_TYPE_KEY,
USER_EMAIL_KEY,
USER_ID_KEY,
USER_NAME_KEY,
)
from microsoft.opentelemetry.a365.core.guardrail_details import GuardrailDetails
from microsoft.opentelemetry.a365.core.guardrail_finding import GuardrailFinding
from microsoft.opentelemetry.a365.core.message_utils import normalize_input_messages, serialize_messages
from microsoft.opentelemetry.a365.core.models.messages import InputMessagesParam
from microsoft.opentelemetry.a365.core.models.user_details import UserDetails
from microsoft.opentelemetry.a365.core.opentelemetry_scope import OpenTelemetryScope
from microsoft.opentelemetry.a365.core.request import Request
from microsoft.opentelemetry.a365.core.span_details import SpanDetails
from microsoft.opentelemetry.a365.core.utils import validate_and_normalize_ip


class ApplyGuardrailScope(OpenTelemetryScope):
"""Provides OpenTelemetry tracing scope for security guardrail evaluations.

Guardian spans SHOULD be children of the operation span they are protecting
(e.g., inference or execute_tool spans). Multiple guardian spans MAY exist
under a single operation span if multiple guardians are chained.

Example usage::

from microsoft.opentelemetry.a365.core import (
ApplyGuardrailScope, GuardrailDetails, AgentDetails,
GuardrailDecisionType, GuardrailTargetType, GuardrailRiskSeverity,
GuardrailFinding,
)

details = GuardrailDetails(
target_type=GuardrailTargetType.LLM_INPUT,
decision_type=GuardrailDecisionType.ALLOW,
guardian_name="Azure Content Safety",
)

with ApplyGuardrailScope.start(details, agent_details) as scope:
# ... run guardrail evaluation ...
scope.record_finding(GuardrailFinding(
risk_category="hate_speech",
risk_severity=GuardrailRiskSeverity.HIGH,
risk_score=0.95,
))
scope.record_decision(GuardrailDecisionType.DENY, "Blocked by policy")
"""

@staticmethod
def start(
details: GuardrailDetails,
agent_details: AgentDetails,
request: Request | None = None,
user_details: UserDetails | None = None,
span_details: SpanDetails | None = None,
) -> "ApplyGuardrailScope":
"""Create and start a new scope for guardrail evaluation tracing.

Args:
details: Guardrail evaluation details (required).
agent_details: Agent identity details (required).
request: Optional request context (conversation ID, channel, content).
user_details: Optional human user details.
span_details: Optional span configuration (parent context, timing, kind).

Returns:
A new ApplyGuardrailScope instance.
"""
return ApplyGuardrailScope(details, agent_details, request, user_details, span_details)

def __init__(
self,
details: GuardrailDetails,
agent_details: AgentDetails,
request: Request | None = None,
user_details: UserDetails | None = None,
span_details: SpanDetails | None = None,
):
"""Initialize the guardrail scope.

Args:
details: Guardrail evaluation details.
agent_details: Agent identity details.
request: Optional request context.
user_details: Optional human user details.
span_details: Optional span configuration.
"""
# Default span kind to INTERNAL for guardrail evaluations
resolved_span_details = (
SpanDetails(
span_kind=span_details.span_kind if span_details and span_details.span_kind else SpanKind.INTERNAL,
parent_context=span_details.parent_context if span_details else None,
start_time=span_details.start_time if span_details else None,
end_time=span_details.end_time if span_details else None,
span_links=span_details.span_links if span_details else None,
)
if span_details
else SpanDetails(span_kind=SpanKind.INTERNAL)
)

super().__init__(
operation_name=APPLY_GUARDRAIL_OPERATION_NAME,
activity_name=self._build_activity_name(details),
agent_details=agent_details,
span_details=resolved_span_details,
)

# Set guardrail-specific attributes
self.set_tag_maybe(GEN_AI_SECURITY_TARGET_TYPE_KEY, details.target_type)
self.set_tag_maybe(GEN_AI_SECURITY_DECISION_TYPE_KEY, details.decision_type)
self.set_tag_maybe(GEN_AI_GUARDIAN_ID_KEY, details.guardian_id)
self.set_tag_maybe(GEN_AI_GUARDIAN_NAME_KEY, details.guardian_name)
self.set_tag_maybe(GEN_AI_GUARDIAN_PROVIDER_NAME_KEY, details.guardian_provider_name)
self.set_tag_maybe(GEN_AI_GUARDIAN_VERSION_KEY, details.guardian_version)
self.set_tag_maybe(GEN_AI_SECURITY_TARGET_ID_KEY, details.target_id)
self.set_tag_maybe(GEN_AI_SECURITY_DECISION_REASON_KEY, details.decision_reason)
self.set_tag_maybe(GEN_AI_SECURITY_DECISION_CODE_KEY, details.decision_code)
self.set_tag_maybe(GEN_AI_SECURITY_POLICY_ID_KEY, details.policy_id)
self.set_tag_maybe(GEN_AI_SECURITY_POLICY_NAME_KEY, details.policy_name)
self.set_tag_maybe(GEN_AI_SECURITY_POLICY_VERSION_KEY, details.policy_version)
self.set_tag_maybe(GEN_AI_SECURITY_CONTENT_INPUT_HASH_KEY, details.content_input_hash)
self.set_tag_maybe(GEN_AI_SECURITY_CONTENT_MODIFIED_KEY, details.content_modified)
self.set_tag_maybe(GEN_AI_SECURITY_EXTERNAL_EVENT_ID_KEY, details.external_event_id)

# Set request context if provided
if request:
self.set_tag_maybe(GEN_AI_CONVERSATION_ID_KEY, request.conversation_id)
if request.channel:
self.set_tag_maybe(CHANNEL_NAME_KEY, request.channel.name)
self.set_tag_maybe(CHANNEL_LINK_KEY, request.channel.link)

# Set user details if provided
if user_details:
self.set_tag_maybe(USER_ID_KEY, user_details.user_id)
self.set_tag_maybe(USER_EMAIL_KEY, user_details.user_email)
self.set_tag_maybe(USER_NAME_KEY, user_details.user_name)
self.set_tag_maybe(
GEN_AI_CALLER_CLIENT_IP_KEY,
validate_and_normalize_ip(user_details.user_client_ip),
)

@staticmethod
def _build_activity_name(details: GuardrailDetails) -> str:
"""Build the span display name from guardrail details."""
if details.guardian_name:
return f"{APPLY_GUARDRAIL_OPERATION_NAME} {details.guardian_name} {details.target_type}"
return f"{APPLY_GUARDRAIL_OPERATION_NAME} {details.target_type}"

def record_decision(self, decision_type: str, reason: str | None = None) -> None:
"""Update the guardrail decision on the span.

Use this to update the decision mid-flight if the initial decision
changes after evaluation completes.

Args:
decision_type: The decision type (use GuardrailDecisionType constants).
reason: Optional human-readable reason for the decision.
"""
self.set_tag_maybe(GEN_AI_SECURITY_DECISION_TYPE_KEY, decision_type)
if reason is not None:
self.set_tag_maybe(GEN_AI_SECURITY_DECISION_REASON_KEY, reason)

def record_content_output(self, output_value: str) -> None:
"""Record the sanitized/modified output content (opt-in).

This is an opt-in field for recording output content after guardrail
processing. Only set this when content capture is explicitly enabled.

Args:
output_value: The output content string.
"""
self.set_tag_maybe(GEN_AI_SECURITY_CONTENT_OUTPUT_VALUE_KEY, output_value)

def record_content_input(self, input_value: InputMessagesParam | str) -> None:
"""Record the input content being evaluated (opt-in).

This is an opt-in field for recording input content sent to the
guardrail. Only set this when content capture is explicitly enabled.

Accepts plain strings or structured ``InputMessages`` containers.
Structured messages are normalized and serialized to a JSON string
before being set as an attribute.

Args:
input_value: The input content as a string or InputMessagesParam.
"""
if isinstance(input_value, str):
self.set_tag_maybe(GEN_AI_SECURITY_CONTENT_INPUT_VALUE_KEY, input_value)
else:
wrapper = normalize_input_messages(input_value)
self.set_tag_maybe(GEN_AI_SECURITY_CONTENT_INPUT_VALUE_KEY, serialize_messages(wrapper))

def record_finding(self, finding: GuardrailFinding) -> None:
"""Record a security finding as a span event.

Each call adds a separate ``microsoft.security.finding`` event to the
span. Multiple findings can be recorded for a single guardrail evaluation.

Args:
finding: The security finding to record.
"""
if not self._span or not self._is_telemetry_enabled():
return

attributes: dict[str, str | float | list[str]] = {
GEN_AI_SECURITY_RISK_CATEGORY_KEY: finding.risk_category,
GEN_AI_SECURITY_RISK_SEVERITY_KEY: finding.risk_severity,
}

if finding.risk_score is not None:
attributes[GEN_AI_SECURITY_RISK_SCORE_KEY] = finding.risk_score
if finding.risk_metadata is not None:
attributes[GEN_AI_SECURITY_RISK_METADATA_KEY] = finding.risk_metadata
if finding.policy_decision_type is not None:
attributes[GEN_AI_SECURITY_POLICY_DECISION_TYPE_KEY] = finding.policy_decision_type
if finding.policy_id is not None:
attributes[GEN_AI_SECURITY_POLICY_ID_KEY] = finding.policy_id
if finding.policy_name is not None:
attributes[GEN_AI_SECURITY_POLICY_NAME_KEY] = finding.policy_name
if finding.policy_version is not None:
attributes[GEN_AI_SECURITY_POLICY_VERSION_KEY] = finding.policy_version

self._span.add_event(GEN_AI_SECURITY_FINDING_EVENT_NAME, attributes=attributes)
26 changes: 26 additions & 0 deletions src/microsoft/opentelemetry/a365/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
EXECUTE_TOOL_OPERATION_NAME = "execute_tool"
OUTPUT_MESSAGES_OPERATION_NAME = "output_messages"
CHAT_OPERATION_NAME = "chat"
APPLY_GUARDRAIL_OPERATION_NAME = "apply_guardrail"

# --- OpenTelemetry semantic conventions ---
ERROR_TYPE_KEY = "error.type"
Expand Down Expand Up @@ -103,6 +104,31 @@
CHANNEL_NAME_KEY = "microsoft.channel.name"
CHANNEL_LINK_KEY = "microsoft.channel.link"

# --- Guardrail / Security ---
GEN_AI_GUARDIAN_ID_KEY = "microsoft.guardian.id"
GEN_AI_GUARDIAN_NAME_KEY = "microsoft.guardian.name"
GEN_AI_GUARDIAN_PROVIDER_NAME_KEY = "microsoft.guardian.provider.name"
GEN_AI_GUARDIAN_VERSION_KEY = "microsoft.guardian.version"
GEN_AI_SECURITY_DECISION_TYPE_KEY = "microsoft.security.decision.type"
GEN_AI_SECURITY_DECISION_REASON_KEY = "microsoft.security.decision.reason"
GEN_AI_SECURITY_DECISION_CODE_KEY = "microsoft.security.decision.code"
GEN_AI_SECURITY_TARGET_TYPE_KEY = "microsoft.security.target.type"
GEN_AI_SECURITY_TARGET_ID_KEY = "microsoft.security.target.id"
GEN_AI_SECURITY_POLICY_ID_KEY = "microsoft.security.policy.id"
GEN_AI_SECURITY_POLICY_NAME_KEY = "microsoft.security.policy.name"
GEN_AI_SECURITY_POLICY_VERSION_KEY = "microsoft.security.policy.version"
GEN_AI_SECURITY_CONTENT_INPUT_HASH_KEY = "microsoft.security.content.input.hash"
GEN_AI_SECURITY_CONTENT_MODIFIED_KEY = "microsoft.security.content.modified"
GEN_AI_SECURITY_EXTERNAL_EVENT_ID_KEY = "microsoft.security.external_event_id"
GEN_AI_SECURITY_CONTENT_INPUT_VALUE_KEY = "microsoft.security.content.input.value"
GEN_AI_SECURITY_CONTENT_OUTPUT_VALUE_KEY = "microsoft.security.content.output.value"
GEN_AI_SECURITY_FINDING_EVENT_NAME = "microsoft.security.finding"
GEN_AI_SECURITY_RISK_CATEGORY_KEY = "microsoft.security.risk.category"
GEN_AI_SECURITY_RISK_SEVERITY_KEY = "microsoft.security.risk.severity"
GEN_AI_SECURITY_RISK_SCORE_KEY = "microsoft.security.risk.score"
GEN_AI_SECURITY_RISK_METADATA_KEY = "microsoft.security.risk.metadata"
GEN_AI_SECURITY_POLICY_DECISION_TYPE_KEY = "microsoft.security.policy.decision.type"

# --- Telemetry SDK attributes ---
TELEMETRY_SDK_NAME_KEY = "telemetry.sdk.name"
TELEMETRY_SDK_LANGUAGE_KEY = "telemetry.sdk.language"
Expand Down
2 changes: 2 additions & 0 deletions src/microsoft/opentelemetry/a365/core/exporters/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
A365_SERVICE_TENANT_ID_ENV,
A365_SUPPRESS_INVOKE_AGENT_INPUT_ENV,
A365_USE_S2S_ENDPOINT_ENV,
APPLY_GUARDRAIL_OPERATION_NAME,
CHAT_OPERATION_NAME,
ENABLE_A365_OBSERVABILITY_EXPORTER,
EXECUTE_TOOL_OPERATION_NAME,
Expand All @@ -61,6 +62,7 @@
EXECUTE_TOOL_OPERATION_NAME,
OUTPUT_MESSAGES_OPERATION_NAME,
CHAT_OPERATION_NAME,
APPLY_GUARDRAIL_OPERATION_NAME,
InferenceOperationType.CHAT.value,
}
)
Expand Down
Loading
Loading