Skip to content

Commit ebc3e9a

Browse files
Add agent version (#220)
* add agent version * add tests --------- Co-authored-by: Nikhil Chitlur Navakiran (from Dev Box) <nikhilc@microsoft.com>
1 parent 6d2e789 commit ebc3e9a

File tree

9 files changed

+159
-0
lines changed

9 files changed

+159
-0
lines changed

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/agent_details.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ class AgentDetails:
3838

3939
provider_name: Optional[str] = None
4040
"""The provider name (e.g., openai, anthropic)."""
41+
42+
agent_version: Optional[str] = None
43+
"""Optional version of the agent (e.g., "1.0.0", "2025-05-01")."""

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
GEN_AI_AGENT_ID_KEY = "gen_ai.agent.id"
3939
GEN_AI_AGENT_NAME_KEY = "gen_ai.agent.name"
4040
GEN_AI_AGENT_DESCRIPTION_KEY = "gen_ai.agent.description"
41+
GEN_AI_AGENT_VERSION_KEY = "gen_ai.agent.version"
4142
GEN_AI_AGENT_PLATFORM_ID_KEY = "microsoft.a365.agent.platform.id"
4243
GEN_AI_AGENT_THOUGHT_PROCESS_KEY = "microsoft.a365.agent.thought.process"
4344
GEN_AI_CONVERSATION_ID_KEY = "gen_ai.conversation.id"
@@ -73,6 +74,7 @@
7374
GEN_AI_CALLER_AGENT_ID_KEY = "microsoft.a365.caller.agent.id"
7475
GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY = "microsoft.a365.caller.agent.blueprint.id"
7576
GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY = "microsoft.a365.caller.agent.platform.id"
77+
GEN_AI_CALLER_AGENT_VERSION_KEY = "microsoft.a365.caller.agent.version"
7678

7779
# Agent-specific dimensions
7880
AGENT_ID_KEY = "gen_ai.agent.id"

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/invoke_agent_scope.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
GEN_AI_CALLER_AGENT_NAME_KEY,
1818
GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY,
1919
GEN_AI_CALLER_AGENT_USER_ID_KEY,
20+
GEN_AI_CALLER_AGENT_VERSION_KEY,
2021
GEN_AI_CALLER_CLIENT_IP_KEY,
2122
GEN_AI_CONVERSATION_ID_KEY,
2223
GEN_AI_INPUT_MESSAGES_KEY,
@@ -162,6 +163,10 @@ def __init__(
162163
GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY,
163164
caller_agent_details.agent_platform_id,
164165
)
166+
self.set_tag_maybe(
167+
GEN_AI_CALLER_AGENT_VERSION_KEY,
168+
caller_agent_details.agent_version,
169+
)
165170

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

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/middleware/baggage_builder.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
GEN_AI_AGENT_EMAIL_KEY,
1818
GEN_AI_AGENT_ID_KEY,
1919
GEN_AI_AGENT_NAME_KEY,
20+
GEN_AI_AGENT_VERSION_KEY,
2021
GEN_AI_CALLER_CLIENT_IP_KEY,
2122
GEN_AI_CONVERSATION_ID_KEY,
2223
GEN_AI_CONVERSATION_ITEM_LINK_KEY,
@@ -154,6 +155,11 @@ def agent_description(self, value: str | None) -> "BaggageBuilder":
154155
self._set(GEN_AI_AGENT_DESCRIPTION_KEY, value)
155156
return self
156157

158+
def agent_version(self, value: str | None) -> "BaggageBuilder":
159+
"""Set the agent version baggage value."""
160+
self._set(GEN_AI_AGENT_VERSION_KEY, value)
161+
return self
162+
157163
def user_name(self, value: str | None) -> "BaggageBuilder":
158164
"""Set the user name baggage value."""
159165
self._set(USER_NAME_KEY, value)

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/opentelemetry_scope.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
GEN_AI_AGENT_ID_KEY,
3434
GEN_AI_AGENT_NAME_KEY,
3535
GEN_AI_AGENT_PLATFORM_ID_KEY,
36+
GEN_AI_AGENT_VERSION_KEY,
3637
GEN_AI_ICON_URI_KEY,
3738
GEN_AI_OPERATION_NAME_KEY,
3839
GEN_AI_OUTPUT_MESSAGES_KEY,
@@ -178,6 +179,7 @@ def __init__(
178179
self.set_tag_maybe(
179180
GEN_AI_AGENT_DESCRIPTION_KEY, agent_details.agent_description
180181
)
182+
self.set_tag_maybe(GEN_AI_AGENT_VERSION_KEY, agent_details.agent_version)
181183
self.set_tag_maybe(GEN_AI_AGENT_AUID_KEY, agent_details.agentic_user_id)
182184
self.set_tag_maybe(GEN_AI_AGENT_EMAIL_KEY, agent_details.agentic_user_email)
183185
self.set_tag_maybe(

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/trace_processor/util.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
consts.GEN_AI_AGENT_ID_KEY, # gen_ai.agent.id
1515
consts.GEN_AI_AGENT_NAME_KEY, # gen_ai.agent.name
1616
consts.GEN_AI_AGENT_DESCRIPTION_KEY, # gen_ai.agent.description
17+
consts.GEN_AI_AGENT_VERSION_KEY, # gen_ai.agent.version
1718
consts.GEN_AI_AGENT_EMAIL_KEY, # microsoft.agent.user.email
1819
consts.GEN_AI_AGENT_BLUEPRINT_ID_KEY, # microsoft.a365.agent.blueprint.id
1920
consts.GEN_AI_AGENT_AUID_KEY, # microsoft.agent.user.id
@@ -41,6 +42,7 @@
4142
consts.GEN_AI_CALLER_AGENT_EMAIL_KEY, # microsoft.a365.caller.agent.user.email
4243
consts.GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY, # microsoft.a365.caller.agent.blueprint.id
4344
consts.GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY, # microsoft.a365.caller.agent.platform.id
45+
consts.GEN_AI_CALLER_AGENT_VERSION_KEY, # microsoft.a365.caller.agent.version
4446
# Server address/port for invoke agent target
4547
consts.SERVER_ADDRESS_KEY, # server.address
4648
consts.SERVER_PORT_KEY, # server.port

tests/observability/core/test_baggage_builder.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
GEN_AI_AGENT_BLUEPRINT_ID_KEY,
1212
GEN_AI_AGENT_EMAIL_KEY,
1313
GEN_AI_AGENT_ID_KEY,
14+
GEN_AI_AGENT_VERSION_KEY,
1415
GEN_AI_CALLER_CLIENT_IP_KEY,
1516
SERVER_ADDRESS_KEY,
1617
SERVER_PORT_KEY,
@@ -364,6 +365,27 @@ def test_invoke_agent_server_sets_address_only_when_port_none(self):
364365
self.assertEqual(current_baggage.get(SERVER_ADDRESS_KEY), address)
365366
self.assertIsNone(current_baggage.get(SERVER_PORT_KEY))
366367

368+
def test_agent_version_method(self):
369+
"""Test agent_version method sets agent version baggage."""
370+
self.assertTrue(hasattr(self.builder, "agent_version"))
371+
self.assertTrue(callable(self.builder.agent_version))
372+
373+
with self.builder.agent_version("1.0.0").build():
374+
current_baggage = baggage.get_all()
375+
self.assertEqual(current_baggage.get(GEN_AI_AGENT_VERSION_KEY), "1.0.0")
376+
377+
def test_agent_version_none_not_set(self):
378+
"""Test agent_version with None does not set baggage."""
379+
with BaggageBuilder().agent_version(None).build():
380+
current_baggage = baggage.get_all()
381+
self.assertIsNone(current_baggage.get(GEN_AI_AGENT_VERSION_KEY))
382+
383+
def test_agent_version_whitespace_not_set(self):
384+
"""Test agent_version with whitespace-only value does not set baggage."""
385+
with BaggageBuilder().agent_version(" ").build():
386+
current_baggage = baggage.get_all()
387+
self.assertIsNone(current_baggage.get(GEN_AI_AGENT_VERSION_KEY))
388+
367389

368390
if __name__ == "__main__":
369391
unittest.main()

tests/observability/core/test_invoke_agent_scope.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@
2424
from microsoft_agents_a365.observability.core.constants import (
2525
CHANNEL_LINK_KEY,
2626
CHANNEL_NAME_KEY,
27+
GEN_AI_AGENT_VERSION_KEY,
28+
GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY,
29+
GEN_AI_CALLER_AGENT_EMAIL_KEY,
30+
GEN_AI_CALLER_AGENT_ID_KEY,
31+
GEN_AI_CALLER_AGENT_NAME_KEY,
32+
GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY,
33+
GEN_AI_CALLER_AGENT_USER_ID_KEY,
34+
GEN_AI_CALLER_AGENT_VERSION_KEY,
2735
GEN_AI_INPUT_MESSAGES_KEY,
2836
SERVER_ADDRESS_KEY,
2937
SERVER_PORT_KEY,
@@ -92,6 +100,7 @@ def setUpClass(cls):
92100
agentic_user_email="agent@contoso.com",
93101
tenant_id="tenant-789",
94102
agent_platform_id="platform-123",
103+
agent_version="2.1.0",
95104
)
96105

97106
def setUp(self):
@@ -271,6 +280,94 @@ def test_span_processor_propagates_server_baggage_for_invoke_agent_span(self):
271280
self.assertEqual(span_attributes.get(SERVER_ADDRESS_KEY), server_address)
272281
self.assertEqual(span_attributes.get(SERVER_PORT_KEY), str(server_port))
273282

283+
def test_agent_version_set_on_span(self):
284+
"""Test that agent_version from AgentDetails is set on span attributes."""
285+
agent_with_version = AgentDetails(
286+
agent_id="versioned-agent",
287+
agent_name="Versioned Agent",
288+
agent_version="1.0.0",
289+
)
290+
scope = InvokeAgentScope.start(
291+
self.test_request,
292+
self.invoke_scope_details,
293+
agent_with_version,
294+
)
295+
if scope is not None:
296+
scope.dispose()
297+
298+
finished_spans = self.span_exporter.get_finished_spans()
299+
self.assertTrue(finished_spans, "Expected at least one span")
300+
span_attributes = getattr(finished_spans[-1], "attributes", {}) or {}
301+
self.assertEqual(span_attributes.get(GEN_AI_AGENT_VERSION_KEY), "1.0.0")
302+
303+
def test_agent_version_not_set_when_none(self):
304+
"""Test that agent_version is not set on span when it is None."""
305+
agent_without_version = AgentDetails(
306+
agent_id="no-version-agent",
307+
agent_name="No Version Agent",
308+
)
309+
scope = InvokeAgentScope.start(
310+
self.test_request,
311+
self.invoke_scope_details,
312+
agent_without_version,
313+
)
314+
if scope is not None:
315+
scope.dispose()
316+
317+
finished_spans = self.span_exporter.get_finished_spans()
318+
self.assertTrue(finished_spans, "Expected at least one span")
319+
span_attributes = getattr(finished_spans[-1], "attributes", {}) or {}
320+
self.assertNotIn(GEN_AI_AGENT_VERSION_KEY, span_attributes)
321+
322+
def test_caller_agent_version_set_on_span(self):
323+
"""Test that caller agent version is emitted on invoke_agent spans."""
324+
caller_with_version = CallerDetails(
325+
user_details=self.user_details,
326+
caller_agent_details=self.caller_agent_details,
327+
)
328+
scope = InvokeAgentScope.start(
329+
self.test_request,
330+
self.invoke_scope_details,
331+
self.agent_details,
332+
caller_details=caller_with_version,
333+
)
334+
if scope is not None:
335+
scope.dispose()
336+
337+
finished_spans = self.span_exporter.get_finished_spans()
338+
self.assertTrue(finished_spans, "Expected at least one span")
339+
span_attributes = getattr(finished_spans[-1], "attributes", {}) or {}
340+
self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_VERSION_KEY), "2.1.0")
341+
342+
def test_caller_agent_details_all_fields_set_on_span(self):
343+
"""Test that all caller agent detail fields are set on span attributes."""
344+
caller_with_agent = CallerDetails(
345+
user_details=self.user_details,
346+
caller_agent_details=self.caller_agent_details,
347+
)
348+
scope = InvokeAgentScope.start(
349+
self.test_request,
350+
self.invoke_scope_details,
351+
self.agent_details,
352+
caller_details=caller_with_agent,
353+
)
354+
if scope is not None:
355+
scope.dispose()
356+
357+
finished_spans = self.span_exporter.get_finished_spans()
358+
self.assertTrue(finished_spans, "Expected at least one span")
359+
span_attributes = getattr(finished_spans[-1], "attributes", {}) or {}
360+
361+
self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_NAME_KEY), "Caller Agent")
362+
self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_ID_KEY), "caller-agent-789")
363+
self.assertEqual(
364+
span_attributes.get(GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY), "blueprint-456"
365+
)
366+
self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_USER_ID_KEY), "auid-123")
367+
self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_EMAIL_KEY), "agent@contoso.com")
368+
self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY), "platform-123")
369+
self.assertEqual(span_attributes.get(GEN_AI_CALLER_AGENT_VERSION_KEY), "2.1.0")
370+
274371

275372
if __name__ == "__main__":
276373
# Run pytest only on the current file

tests/observability/core/test_span_processor.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from microsoft_agents_a365.observability.core.constants import (
88
GEN_AI_AGENT_ID_KEY,
9+
GEN_AI_AGENT_VERSION_KEY,
910
TENANT_ID_KEY,
1011
)
1112
from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder
@@ -44,6 +45,25 @@ def test_on_end_calls_super(self):
4445
except Exception as e:
4546
self.fail(f"on_end raised an exception: {e}")
4647

48+
def test_agent_version_baggage_propagates_to_span(self):
49+
"""Test that agent version baggage is propagated to span attributes."""
50+
self.mock_span.attributes = {}
51+
52+
with (
53+
BaggageBuilder()
54+
.tenant_id("test-tenant")
55+
.agent_id("test-agent")
56+
.agent_version("3.0.0")
57+
.build()
58+
):
59+
self.processor.on_start(self.mock_span, context.get_current())
60+
61+
calls = self.mock_span.set_attribute.call_args_list
62+
call_dict = {call[0][0]: call[0][1] for call in calls}
63+
self.assertEqual(call_dict.get(GEN_AI_AGENT_VERSION_KEY), "3.0.0")
64+
self.assertEqual(call_dict.get(TENANT_ID_KEY), "test-tenant")
65+
self.assertEqual(call_dict.get(GEN_AI_AGENT_ID_KEY), "test-agent")
66+
4767

4868
if __name__ == "__main__":
4969
unittest.main()

0 commit comments

Comments
 (0)