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 @@ -38,3 +38,6 @@ class AgentDetails:

provider_name: Optional[str] = None
"""The provider name (e.g., openai, anthropic)."""

agent_version: Optional[str] = None
"""Optional version of the agent (e.g., "1.0.0", "2025-05-01")."""
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
GEN_AI_AGENT_ID_KEY = "gen_ai.agent.id"
GEN_AI_AGENT_NAME_KEY = "gen_ai.agent.name"
GEN_AI_AGENT_DESCRIPTION_KEY = "gen_ai.agent.description"
GEN_AI_AGENT_VERSION_KEY = "gen_ai.agent.version"
GEN_AI_AGENT_PLATFORM_ID_KEY = "microsoft.a365.agent.platform.id"
GEN_AI_AGENT_THOUGHT_PROCESS_KEY = "microsoft.a365.agent.thought.process"
GEN_AI_CONVERSATION_ID_KEY = "gen_ai.conversation.id"
Expand Down Expand Up @@ -73,6 +74,7 @@
GEN_AI_CALLER_AGENT_ID_KEY = "microsoft.a365.caller.agent.id"
GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY = "microsoft.a365.caller.agent.blueprint.id"
GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY = "microsoft.a365.caller.agent.platform.id"
GEN_AI_CALLER_AGENT_VERSION_KEY = "microsoft.a365.caller.agent.version"

# Agent-specific dimensions
AGENT_ID_KEY = "gen_ai.agent.id"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
GEN_AI_CALLER_AGENT_NAME_KEY,
GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY,
GEN_AI_CALLER_AGENT_USER_ID_KEY,
GEN_AI_CALLER_AGENT_VERSION_KEY,
GEN_AI_CALLER_CLIENT_IP_KEY,
GEN_AI_CONVERSATION_ID_KEY,
GEN_AI_INPUT_MESSAGES_KEY,
Expand Down Expand Up @@ -162,6 +163,10 @@ def __init__(
GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY,
caller_agent_details.agent_platform_id,
)
self.set_tag_maybe(
GEN_AI_CALLER_AGENT_VERSION_KEY,
caller_agent_details.agent_version,
)

def record_response(self, response: str) -> None:
"""Record response information for telemetry tracking.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
GEN_AI_AGENT_EMAIL_KEY,
GEN_AI_AGENT_ID_KEY,
GEN_AI_AGENT_NAME_KEY,
GEN_AI_AGENT_VERSION_KEY,
GEN_AI_CALLER_CLIENT_IP_KEY,
GEN_AI_CONVERSATION_ID_KEY,
GEN_AI_CONVERSATION_ITEM_LINK_KEY,
Expand Down Expand Up @@ -154,6 +155,11 @@ def agent_description(self, value: str | None) -> "BaggageBuilder":
self._set(GEN_AI_AGENT_DESCRIPTION_KEY, value)
return self

def agent_version(self, value: str | None) -> "BaggageBuilder":
"""Set the agent version baggage value."""
self._set(GEN_AI_AGENT_VERSION_KEY, value)
return self

def user_name(self, value: str | None) -> "BaggageBuilder":
"""Set the user name baggage value."""
self._set(USER_NAME_KEY, value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
GEN_AI_AGENT_ID_KEY,
GEN_AI_AGENT_NAME_KEY,
GEN_AI_AGENT_PLATFORM_ID_KEY,
GEN_AI_AGENT_VERSION_KEY,
GEN_AI_ICON_URI_KEY,
GEN_AI_OPERATION_NAME_KEY,
GEN_AI_OUTPUT_MESSAGES_KEY,
Expand Down Expand Up @@ -178,6 +179,7 @@ def __init__(
self.set_tag_maybe(
GEN_AI_AGENT_DESCRIPTION_KEY, agent_details.agent_description
)
self.set_tag_maybe(GEN_AI_AGENT_VERSION_KEY, agent_details.agent_version)
self.set_tag_maybe(GEN_AI_AGENT_AUID_KEY, agent_details.agentic_user_id)
self.set_tag_maybe(GEN_AI_AGENT_EMAIL_KEY, agent_details.agentic_user_email)
self.set_tag_maybe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
consts.GEN_AI_AGENT_ID_KEY, # gen_ai.agent.id
consts.GEN_AI_AGENT_NAME_KEY, # gen_ai.agent.name
consts.GEN_AI_AGENT_DESCRIPTION_KEY, # gen_ai.agent.description
consts.GEN_AI_AGENT_VERSION_KEY, # gen_ai.agent.version
consts.GEN_AI_AGENT_EMAIL_KEY, # microsoft.agent.user.email
consts.GEN_AI_AGENT_BLUEPRINT_ID_KEY, # microsoft.a365.agent.blueprint.id
consts.GEN_AI_AGENT_AUID_KEY, # microsoft.agent.user.id
Expand Down Expand Up @@ -41,6 +42,7 @@
consts.GEN_AI_CALLER_AGENT_EMAIL_KEY, # microsoft.a365.caller.agent.user.email
consts.GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY, # microsoft.a365.caller.agent.blueprint.id
consts.GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY, # microsoft.a365.caller.agent.platform.id
consts.GEN_AI_CALLER_AGENT_VERSION_KEY, # microsoft.a365.caller.agent.version
# Server address/port for invoke agent target
consts.SERVER_ADDRESS_KEY, # server.address
consts.SERVER_PORT_KEY, # server.port
Expand Down
22 changes: 22 additions & 0 deletions tests/observability/core/test_baggage_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
GEN_AI_AGENT_BLUEPRINT_ID_KEY,
GEN_AI_AGENT_EMAIL_KEY,
GEN_AI_AGENT_ID_KEY,
GEN_AI_AGENT_VERSION_KEY,
GEN_AI_CALLER_CLIENT_IP_KEY,
SERVER_ADDRESS_KEY,
SERVER_PORT_KEY,
Expand Down Expand Up @@ -364,6 +365,27 @@ def test_invoke_agent_server_sets_address_only_when_port_none(self):
self.assertEqual(current_baggage.get(SERVER_ADDRESS_KEY), address)
self.assertIsNone(current_baggage.get(SERVER_PORT_KEY))

def test_agent_version_method(self):
"""Test agent_version method sets agent version baggage."""
self.assertTrue(hasattr(self.builder, "agent_version"))
self.assertTrue(callable(self.builder.agent_version))

with self.builder.agent_version("1.0.0").build():
current_baggage = baggage.get_all()
self.assertEqual(current_baggage.get(GEN_AI_AGENT_VERSION_KEY), "1.0.0")

def test_agent_version_none_not_set(self):
"""Test agent_version with None does not set baggage."""
with BaggageBuilder().agent_version(None).build():
current_baggage = baggage.get_all()
self.assertIsNone(current_baggage.get(GEN_AI_AGENT_VERSION_KEY))

def test_agent_version_whitespace_not_set(self):
"""Test agent_version with whitespace-only value does not set baggage."""
with BaggageBuilder().agent_version(" ").build():
current_baggage = baggage.get_all()
self.assertIsNone(current_baggage.get(GEN_AI_AGENT_VERSION_KEY))


if __name__ == "__main__":
unittest.main()
97 changes: 97 additions & 0 deletions tests/observability/core/test_invoke_agent_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
from microsoft_agents_a365.observability.core.constants import (
CHANNEL_LINK_KEY,
CHANNEL_NAME_KEY,
GEN_AI_AGENT_VERSION_KEY,
GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY,
GEN_AI_CALLER_AGENT_EMAIL_KEY,
GEN_AI_CALLER_AGENT_ID_KEY,
GEN_AI_CALLER_AGENT_NAME_KEY,
GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY,
GEN_AI_CALLER_AGENT_USER_ID_KEY,
GEN_AI_CALLER_AGENT_VERSION_KEY,
GEN_AI_INPUT_MESSAGES_KEY,
SERVER_ADDRESS_KEY,
SERVER_PORT_KEY,
Expand Down Expand Up @@ -92,6 +100,7 @@ def setUpClass(cls):
agentic_user_email="agent@contoso.com",
tenant_id="tenant-789",
agent_platform_id="platform-123",
agent_version="2.1.0",
)

def setUp(self):
Expand Down Expand Up @@ -271,6 +280,94 @@ def test_span_processor_propagates_server_baggage_for_invoke_agent_span(self):
self.assertEqual(span_attributes.get(SERVER_ADDRESS_KEY), server_address)
self.assertEqual(span_attributes.get(SERVER_PORT_KEY), str(server_port))

def test_agent_version_set_on_span(self):
"""Test that agent_version from AgentDetails is set on span attributes."""
agent_with_version = AgentDetails(
agent_id="versioned-agent",
agent_name="Versioned Agent",
agent_version="1.0.0",
)
scope = InvokeAgentScope.start(
self.test_request,
self.invoke_scope_details,
agent_with_version,
)
if scope is not None:
scope.dispose()

finished_spans = self.span_exporter.get_finished_spans()
self.assertTrue(finished_spans, "Expected at least one span")
span_attributes = getattr(finished_spans[-1], "attributes", {}) or {}
self.assertEqual(span_attributes.get(GEN_AI_AGENT_VERSION_KEY), "1.0.0")

def test_agent_version_not_set_when_none(self):
"""Test that agent_version is not set on span when it is None."""
agent_without_version = AgentDetails(
agent_id="no-version-agent",
agent_name="No Version Agent",
)
scope = InvokeAgentScope.start(
self.test_request,
self.invoke_scope_details,
agent_without_version,
)
if scope is not None:
scope.dispose()

finished_spans = self.span_exporter.get_finished_spans()
self.assertTrue(finished_spans, "Expected at least one span")
span_attributes = getattr(finished_spans[-1], "attributes", {}) or {}
self.assertNotIn(GEN_AI_AGENT_VERSION_KEY, span_attributes)

def test_caller_agent_version_set_on_span(self):
"""Test that caller agent version is emitted on invoke_agent spans."""
caller_with_version = CallerDetails(
user_details=self.user_details,
caller_agent_details=self.caller_agent_details,
)
scope = InvokeAgentScope.start(
self.test_request,
self.invoke_scope_details,
self.agent_details,
caller_details=caller_with_version,
)
if scope is not None:
scope.dispose()

finished_spans = self.span_exporter.get_finished_spans()
self.assertTrue(finished_spans, "Expected at least one span")
span_attributes = getattr(finished_spans[-1], "attributes", {}) or {}
self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_VERSION_KEY), "2.1.0")

def test_caller_agent_details_all_fields_set_on_span(self):
"""Test that all caller agent detail fields are set on span attributes."""
caller_with_agent = CallerDetails(
user_details=self.user_details,
caller_agent_details=self.caller_agent_details,
)
scope = InvokeAgentScope.start(
self.test_request,
self.invoke_scope_details,
self.agent_details,
caller_details=caller_with_agent,
)
if scope is not None:
scope.dispose()

finished_spans = self.span_exporter.get_finished_spans()
self.assertTrue(finished_spans, "Expected at least one span")
span_attributes = getattr(finished_spans[-1], "attributes", {}) or {}

self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_NAME_KEY), "Caller Agent")
self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_ID_KEY), "caller-agent-789")
self.assertEqual(
span_attributes.get(GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY), "blueprint-456"
)
self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_USER_ID_KEY), "auid-123")
self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_EMAIL_KEY), "agent@contoso.com")
self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY), "platform-123")
self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_VERSION_KEY), "2.1.0")


if __name__ == "__main__":
# Run pytest only on the current file
Expand Down
20 changes: 20 additions & 0 deletions tests/observability/core/test_span_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from microsoft_agents_a365.observability.core.constants import (
GEN_AI_AGENT_ID_KEY,
GEN_AI_AGENT_VERSION_KEY,
TENANT_ID_KEY,
)
from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder
Expand Down Expand Up @@ -44,6 +45,25 @@ def test_on_end_calls_super(self):
except Exception as e:
self.fail(f"on_end raised an exception: {e}")

def test_agent_version_baggage_propagates_to_span(self):
"""Test that agent version baggage is propagated to span attributes."""
self.mock_span.attributes = {}

with (
BaggageBuilder()
.tenant_id("test-tenant")
.agent_id("test-agent")
.agent_version("3.0.0")
.build()
):
self.processor.on_start(self.mock_span, context.get_current())

calls = self.mock_span.set_attribute.call_args_list
call_dict = {call[0][0]: call[0][1] for call in calls}
self.assertEqual(call_dict.get(GEN_AI_AGENT_VERSION_KEY), "3.0.0")
self.assertEqual(call_dict.get(TENANT_ID_KEY), "test-tenant")
self.assertEqual(call_dict.get(GEN_AI_AGENT_ID_KEY), "test-agent")


if __name__ == "__main__":
unittest.main()
Loading