Source code for microsoft.opentelemetry.a365.core.execute_tool_scope

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from opentelemetry.trace import SpanKind

from microsoft.opentelemetry.a365.core.agent_details import AgentDetails
from microsoft.opentelemetry.a365.core.constants import (
    CHANNEL_LINK_KEY,
    CHANNEL_NAME_KEY,
    EXECUTE_TOOL_OPERATION_NAME,
    GEN_AI_CALLER_CLIENT_IP_KEY,
    GEN_AI_CONVERSATION_ID_KEY,
    GEN_AI_TOOL_ARGS_KEY,
    GEN_AI_TOOL_CALL_ID_KEY,
    GEN_AI_TOOL_CALL_RESULT_KEY,
    GEN_AI_TOOL_DESCRIPTION_KEY,
    GEN_AI_TOOL_NAME_KEY,
    GEN_AI_TOOL_TYPE_KEY,
    SERVER_ADDRESS_KEY,
    SERVER_PORT_KEY,
    USER_EMAIL_KEY,
    USER_ID_KEY,
    USER_NAME_KEY,
)
from microsoft.opentelemetry.a365.core.utils import safe_json_dumps, validate_and_normalize_ip
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.tool_call_details import ToolCallDetails


[docs] class ExecuteToolScope(OpenTelemetryScope): """Provides OpenTelemetry tracing scope for AI tool execution operations."""
[docs] @staticmethod def start( request: Request, details: ToolCallDetails, agent_details: AgentDetails, user_details: UserDetails | None = None, span_details: SpanDetails | None = None, ) -> "ExecuteToolScope": """Creates and starts a new scope for tool execution tracing. Args: request: Request details for the tool execution details: The details of the tool call agent_details: The details of the agent making the call user_details: Optional human user details span_details: Optional span configuration (parent context, timing, kind) Returns: A new ExecuteToolScope instance """ return ExecuteToolScope( request, details, agent_details, user_details, span_details, )
def __init__( self, request: Request, details: ToolCallDetails, agent_details: AgentDetails, user_details: UserDetails | None = None, span_details: SpanDetails | None = None, ): """Initialize the tool execution scope. Args: request: Request details for the tool execution details: The details of the tool call agent_details: The details of the agent making the call user_details: Optional human user details span_details: Optional span configuration (parent context, timing, kind) """ # spanKind defaults to INTERNAL; allow override via span_details 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=EXECUTE_TOOL_OPERATION_NAME, activity_name=f"{EXECUTE_TOOL_OPERATION_NAME} {details.tool_name}", agent_details=agent_details, span_details=resolved_span_details, ) # Extract details tool_name = details.tool_name arguments = details.arguments tool_call_id = details.tool_call_id description = details.description tool_type = details.tool_type endpoint = details.endpoint self.set_tag_maybe(GEN_AI_TOOL_NAME_KEY, tool_name) if arguments is not None: serialized = safe_json_dumps(arguments) if isinstance(arguments, dict) else arguments self.set_tag_maybe(GEN_AI_TOOL_ARGS_KEY, serialized) self.set_tag_maybe(GEN_AI_TOOL_TYPE_KEY, tool_type) self.set_tag_maybe(GEN_AI_TOOL_CALL_ID_KEY, tool_call_id) self.set_tag_maybe(GEN_AI_TOOL_DESCRIPTION_KEY, description) self.set_tag_maybe(GEN_AI_CONVERSATION_ID_KEY, request.conversation_id) if endpoint: self.set_tag_maybe(SERVER_ADDRESS_KEY, endpoint.hostname) if endpoint.port and endpoint.port != 443: self.set_tag_maybe(SERVER_PORT_KEY, endpoint.port) # Set request metadata if provided 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), )
[docs] def record_response(self, result: dict[str, object] | str) -> None: # pylint: disable=arguments-renamed """Record the tool call result for telemetry tracking. Per OTEL spec, the result is expected to be an object. If a string is provided, it is recorded as-is (JSON string fallback). If a dict is provided, it is serialized to JSON. Args: result: Tool call result as a structured dict or JSON string """ serialized = safe_json_dumps(result) if isinstance(result, dict) else result self.set_tag_maybe(GEN_AI_TOOL_CALL_RESULT_KEY, serialized)