Skip to content

Commit 9ddca17

Browse files
CopilotnikhilNava
andcommitted
Add parent_id parameter to OutputScope
Similar to the .NET ExecuteToolScope, the OutputScope now accepts an optional parent_id parameter to link spans to upstream operations. Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com>
1 parent 53bd58d commit 9ddca17

2 files changed

Lines changed: 63 additions & 3 deletions

File tree

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/spans_scopes/output_scope.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Licensed under the MIT License.
33

44
from ..agent_details import AgentDetails
5-
from ..constants import GEN_AI_OUTPUT_MESSAGES_KEY
5+
from ..constants import CUSTOM_PARENT_SPAN_ID_KEY, GEN_AI_OUTPUT_MESSAGES_KEY
66
from ..models.response import Response
77
from ..opentelemetry_scope import OpenTelemetryScope
88
from ..tenant_details import TenantDetails
@@ -19,31 +19,37 @@ def start(
1919
agent_details: AgentDetails,
2020
tenant_details: TenantDetails,
2121
response: Response,
22+
parent_id: str | None = None,
2223
) -> "OutputScope":
2324
"""Creates and starts a new scope for output tracing.
2425
2526
Args:
2627
agent_details: The details of the agent
2728
tenant_details: The details of the tenant
2829
response: The response details from the agent
30+
parent_id: Optional parent Activity ID used to link this span to an upstream
31+
operation
2932
3033
Returns:
3134
A new OutputScope instance
3235
"""
33-
return OutputScope(agent_details, tenant_details, response)
36+
return OutputScope(agent_details, tenant_details, response, parent_id)
3437

3538
def __init__(
3639
self,
3740
agent_details: AgentDetails,
3841
tenant_details: TenantDetails,
3942
response: Response,
43+
parent_id: str | None = None,
4044
):
4145
"""Initialize the output scope.
4246
4347
Args:
4448
agent_details: The details of the agent
4549
tenant_details: The details of the tenant
4650
response: The response details from the agent
51+
parent_id: Optional parent Activity ID used to link this span to an upstream
52+
operation
4753
"""
4854
super().__init__(
4955
kind="Client",
@@ -53,6 +59,9 @@ def __init__(
5359
tenant_details=tenant_details,
5460
)
5561

62+
# Set parent ID if provided
63+
self.set_tag_maybe(CUSTOM_PARENT_SPAN_ID_KEY, parent_id)
64+
5665
# Set response messages
5766
self.set_tag_maybe(GEN_AI_OUTPUT_MESSAGES_KEY, safe_json_dumps(response.messages))
5867

tests/observability/core/test_output_scope.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
get_tracer_provider,
1515
)
1616
from microsoft_agents_a365.observability.core.config import _telemetry_manager
17-
from microsoft_agents_a365.observability.core.constants import GEN_AI_OUTPUT_MESSAGES_KEY
17+
from microsoft_agents_a365.observability.core.constants import (
18+
CUSTOM_PARENT_SPAN_ID_KEY,
19+
GEN_AI_OUTPUT_MESSAGES_KEY,
20+
)
1821
from microsoft_agents_a365.observability.core.models.response import Response
1922
from microsoft_agents_a365.observability.core.opentelemetry_scope import OpenTelemetryScope
2023
from microsoft_agents_a365.observability.core.spans_scopes.output_scope import OutputScope
@@ -192,6 +195,54 @@ def test_output_scope_span_name(self):
192195
self.assertIn("output_messages", span.name)
193196
self.assertIn(self.agent_details.agent_id, span.name)
194197

198+
def test_output_scope_with_parent_id(self):
199+
"""Test that OutputScope records parent_id when provided."""
200+
response = Response(messages=["Test message with parent"])
201+
parent_id = "00-1234567890abcdef1234567890abcdef-abcdefabcdef1234-01"
202+
203+
scope = OutputScope.start(
204+
self.agent_details, self.tenant_details, response, parent_id=parent_id
205+
)
206+
207+
if scope is not None:
208+
scope.dispose()
209+
210+
finished_spans = self.span_exporter.get_finished_spans()
211+
self.assertTrue(finished_spans, "Expected at least one span to be created")
212+
213+
span = finished_spans[-1]
214+
span_attributes = getattr(span, "attributes", {}) or {}
215+
216+
# Verify the parent ID is set as a span attribute
217+
self.assertIn(
218+
CUSTOM_PARENT_SPAN_ID_KEY,
219+
span_attributes,
220+
"Expected custom parent span ID to be set on span",
221+
)
222+
self.assertEqual(span_attributes[CUSTOM_PARENT_SPAN_ID_KEY], parent_id)
223+
224+
def test_output_scope_without_parent_id(self):
225+
"""Test that OutputScope doesn't set parent_id attribute when not provided."""
226+
response = Response(messages=["Test message without parent"])
227+
228+
scope = OutputScope.start(self.agent_details, self.tenant_details, response)
229+
230+
if scope is not None:
231+
scope.dispose()
232+
233+
finished_spans = self.span_exporter.get_finished_spans()
234+
self.assertTrue(finished_spans, "Expected at least one span to be created")
235+
236+
span = finished_spans[-1]
237+
span_attributes = getattr(span, "attributes", {}) or {}
238+
239+
# Verify the parent ID attribute is NOT set when not provided
240+
self.assertNotIn(
241+
CUSTOM_PARENT_SPAN_ID_KEY,
242+
span_attributes,
243+
"Expected custom parent span ID NOT to be set when not provided",
244+
)
245+
195246

196247
if __name__ == "__main__":
197248
# Run pytest only on the current file

0 commit comments

Comments
 (0)