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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from .constants import (
EXECUTE_TOOL_OPERATION_NAME,
GEN_AI_EVENT_CONTENT,
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY,
GEN_AI_EXECUTION_SOURCE_NAME_KEY,
GEN_AI_TOOL_ARGS_KEY,
GEN_AI_TOOL_CALL_ID_KEY,
GEN_AI_TOOL_DESCRIPTION_KEY,
Expand All @@ -14,6 +16,7 @@
SERVER_PORT_KEY,
)
from .opentelemetry_scope import OpenTelemetryScope
from .request import Request
from .tenant_details import TenantDetails
from .tool_call_details import ToolCallDetails

Expand All @@ -26,31 +29,35 @@ def start(
details: ToolCallDetails,
agent_details: AgentDetails,
tenant_details: TenantDetails,
request: Request | None = None,
) -> "ExecuteToolScope":
"""Creates and starts a new scope for tool execution tracing.

Args:
details: The details of the tool call
agent_details: The details of the agent making the call
tenant_details: The details of the tenant
request: Optional request details for additional context

Returns:
A new ExecuteToolScope instance
"""
return ExecuteToolScope(details, agent_details, tenant_details)
return ExecuteToolScope(details, agent_details, tenant_details, request)

def __init__(
self,
details: ToolCallDetails,
agent_details: AgentDetails,
tenant_details: TenantDetails,
request: Request | None = None,
):
"""Initialize the tool execution scope.

Args:
details: The details of the tool call
agent_details: The details of the agent making the call
tenant_details: The details of the tenant
request: Optional request details for additional context
"""
super().__init__(
kind="Internal",
Expand Down Expand Up @@ -79,6 +86,13 @@ def __init__(
if endpoint.port and endpoint.port != 443:
self.set_tag_maybe(SERVER_PORT_KEY, endpoint.port)

# Set request metadata if provided
if request and request.source_metadata:
self.set_tag_maybe(GEN_AI_EXECUTION_SOURCE_NAME_KEY, request.source_metadata.name)
self.set_tag_maybe(
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, request.source_metadata.description
)

def record_response(self, response: str) -> None:
"""Records response information for telemetry tracking.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from .agent_details import AgentDetails
from .constants import (
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY,
GEN_AI_EXECUTION_SOURCE_NAME_KEY,
GEN_AI_INPUT_MESSAGES_KEY,
GEN_AI_OPERATION_NAME_KEY,
GEN_AI_OUTPUT_MESSAGES_KEY,
Expand Down Expand Up @@ -90,6 +92,13 @@ def __init__(
)
self.set_tag_maybe(GEN_AI_RESPONSE_ID_KEY, details.responseId)

# Set request metadata if provided
if request and request.source_metadata:
self.set_tag_maybe(GEN_AI_EXECUTION_SOURCE_NAME_KEY, request.source_metadata.name)
self.set_tag_maybe(
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, request.source_metadata.description
)

def record_input_messages(self, messages: List[str]) -> None:
"""Records the input messages for telemetry tracking.

Expand Down
74 changes: 73 additions & 1 deletion tests/observability/core/test_execute_tool_scope.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

import os
from pathlib import Path
import sys
import unittest
import pytest

from microsoft_agents_a365.observability.core import (
AgentDetails,
ExecutionType,
ExecuteToolScope,
Request,
SourceMetadata,
TenantDetails,
ToolCallDetails,
configure,
get_tracer_provider,
)
from microsoft_agents_a365.observability.core.constants import (
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY,
GEN_AI_EXECUTION_SOURCE_NAME_KEY,
)
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter


class TestExecuteToolScope(unittest.TestCase):
Expand All @@ -19,6 +33,8 @@ class TestExecuteToolScope(unittest.TestCase):
def setUpClass(cls):
"""Set up test environment once for all tests."""
# Configure Microsoft Agent 365 for testing
os.environ["ENABLE_A365_OBSERVABILITY"] = "true"

configure(
service_name="test-execute-tool-service",
service_namespace="test-namespace",
Expand All @@ -37,6 +53,19 @@ def setUpClass(cls):
description="Get current weather information for a location",
)

def setUp(self):
super().setUp()

# Set up tracer to capture spans
self.span_exporter = InMemorySpanExporter()
tracer_provider = get_tracer_provider()
tracer_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter))

def tearDown(self):
super().tearDown()

self.span_exporter.clear()

def test_record_response_method_exists(self):
"""Test that record_response method exists on ExecuteToolScope."""
scope = ExecuteToolScope.start(self.tool_details, self.agent_details, self.tenant_details)
Expand All @@ -47,6 +76,49 @@ def test_record_response_method_exists(self):
self.assertTrue(callable(scope.record_response))
scope.dispose()

def test_request_metadata_set_on_span(self):
"""Test that request source metadata is set on span attributes."""
request = Request(
content="Execute tool with request metadata",
execution_type=ExecutionType.AGENT_TO_AGENT,
session_id="session-xyz",
source_metadata=SourceMetadata(name="Channel 1", description="Link to channel"),
)

scope = ExecuteToolScope.start(
self.tool_details, self.agent_details, self.tenant_details, request
)

if scope is not None:
scope.dispose()

finished_spans = self.span_exporter.get_finished_spans()
self.assertTrue(finished_spans, "Expected at least one span to be created")

span = finished_spans[-1]
span_attributes = getattr(span, "attributes", {}) or {}

self.assertIn(
GEN_AI_EXECUTION_SOURCE_NAME_KEY,
span_attributes,
"Expected source name to be set on span",
)
self.assertEqual(
span_attributes[GEN_AI_EXECUTION_SOURCE_NAME_KEY],
request.source_metadata.name,
)

self.assertIn(
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY,
span_attributes,
"Expected source description to be set on span",
)
self.assertEqual(
span_attributes[GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY],
request.source_metadata.description,
)


if __name__ == "__main__":
unittest.main(verbosity=2)
# Run pytest only on the current file
sys.exit(pytest.main([str(Path(__file__))] + sys.argv[1:]))
Loading
Loading