From 941fc82cec7fba212d702599b35ed86dabf8f77e Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Wed, 28 Oct 2026 09:46:33 -0600 Subject: [PATCH 1/3] Propagate request sessions to Agent365 tool and inference scopes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfa03945-cb0e-4471-ba78-5980a3098d0b --- A365_DOCUMENTATION.md | 6 +++++ CHANGELOG.md | 4 ++++ .../a365/core/execute_tool_scope.py | 2 ++ .../a365/core/inference_scope.py | 2 ++ tests/a365/test_execute_tool_scope.py | 22 +++++++++++++++++++ tests/a365/test_inference_scope.py | 17 ++++++++++++++ 6 files changed, 53 insertions(+) diff --git a/A365_DOCUMENTATION.md b/A365_DOCUMENTATION.md index 4e4489694..7d8f88370 100644 --- a/A365_DOCUMENTATION.md +++ b/A365_DOCUMENTATION.md @@ -458,6 +458,12 @@ Supported semantic attributes: ### ExecuteToolScope +Pass `Request(session_id="session-123")` to correlate execute-tool and inference +spans using `microsoft.session.id`, as with invoke-agent spans. The session ID is +recorded directly from the request without requiring a baggage builder or span +processor. Omitting it leaves the attribute unset, and `conversation_id` remains +an independent correlation field. + ```python from microsoft.opentelemetry.a365.core import ExecuteToolScope, ToolCallDetails, Request diff --git a/CHANGELOG.md b/CHANGELOG.md index 1564ad0fc..3b0ca5c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Release History # Unreleased +### Bugs Fixed +- Propagate `Request.session_id` directly to Agent365 execute-tool and inference spans, + even without a baggage processor (microsoft/opentelemetry-distro-dotnet#116). + ### Other Changes - Update the `AZURE_OPENAI_API_VERSION` fallback version in integration tests diff --git a/src/microsoft/opentelemetry/a365/core/execute_tool_scope.py b/src/microsoft/opentelemetry/a365/core/execute_tool_scope.py index 72593d26a..180a6ac69 100644 --- a/src/microsoft/opentelemetry/a365/core/execute_tool_scope.py +++ b/src/microsoft/opentelemetry/a365/core/execute_tool_scope.py @@ -20,6 +20,7 @@ GEN_AI_TOOL_TYPE_KEY, SERVER_ADDRESS_KEY, SERVER_PORT_KEY, + SESSION_ID_KEY, USER_EMAIL_KEY, USER_ID_KEY, USER_NAME_KEY, @@ -146,6 +147,7 @@ def __init__( 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) + self.set_tag_maybe(SESSION_ID_KEY, request.session_id) if endpoint: self.set_tag_maybe(SERVER_ADDRESS_KEY, endpoint.hostname) diff --git a/src/microsoft/opentelemetry/a365/core/inference_scope.py b/src/microsoft/opentelemetry/a365/core/inference_scope.py index 48640bdf2..48b59c179 100644 --- a/src/microsoft/opentelemetry/a365/core/inference_scope.py +++ b/src/microsoft/opentelemetry/a365/core/inference_scope.py @@ -24,6 +24,7 @@ GEN_AI_USAGE_OUTPUT_TOKENS_KEY, SERVER_ADDRESS_KEY, SERVER_PORT_KEY, + SESSION_ID_KEY, USER_EMAIL_KEY, USER_ID_KEY, USER_NAME_KEY, @@ -109,6 +110,7 @@ def __init__( if request.content is not None: self.record_input_messages(request.content) self.set_tag_maybe(GEN_AI_CONVERSATION_ID_KEY, request.conversation_id) + self.set_tag_maybe(SESSION_ID_KEY, request.session_id) self.set_tag_maybe(GEN_AI_OPERATION_NAME_KEY, operation_name) self.set_tag_maybe(GEN_AI_REQUEST_MODEL_KEY, details.model) diff --git a/tests/a365/test_execute_tool_scope.py b/tests/a365/test_execute_tool_scope.py index da18c4b6b..008c0dd0a 100644 --- a/tests/a365/test_execute_tool_scope.py +++ b/tests/a365/test_execute_tool_scope.py @@ -6,6 +6,7 @@ import unittest from unittest.mock import patch +import pytest from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider @@ -259,3 +260,24 @@ def test_unserializable_typed_payloads_are_safe_when_telemetry_is_disabled(self) self.assertIsNone(scope._span) self.assertIsNone(raw_scope._span) + + +@pytest.mark.parametrize("session_id", ["session-123", None]) +def test_execute_tool_records_request_session_without_a_baggage_processor(monkeypatch, session_id): + provider = TracerProvider() + monkeypatch.setenv("ENABLE_OBSERVABILITY", "true") + monkeypatch.setattr(ExecuteToolScope, "_tracer", provider.get_tracer("Agent365Sdk")) + try: + with ExecuteToolScope.start( + Request(session_id=session_id, conversation_id="conversation-123"), + ToolCallDetails(tool_name="lookup"), + AgentDetails("agent"), + ) as scope: + attrs = dict(scope._span.attributes) + assert attrs["gen_ai.conversation.id"] == "conversation-123" + if session_id is None: + assert "microsoft.session.id" not in attrs + else: + assert attrs["microsoft.session.id"] == session_id + finally: + provider.shutdown() diff --git a/tests/a365/test_inference_scope.py b/tests/a365/test_inference_scope.py index 45fa25cbf..725649016 100644 --- a/tests/a365/test_inference_scope.py +++ b/tests/a365/test_inference_scope.py @@ -4,6 +4,8 @@ import os from typing import Any, cast +import pytest + from microsoft.opentelemetry.a365.core import ( AgentDetails, InferenceCallDetails, @@ -113,3 +115,18 @@ def test_inference_scope_records_late_response_details() -> None: assert attrs["gen_ai.usage.cache_write.input_tokens"] == 6 finally: scope.dispose() + + +@pytest.mark.parametrize("session_id", ["session-123", None]) +def test_inference_records_request_session_without_a_baggage_processor(session_id) -> None: + with InferenceScope.start( + Request(session_id=session_id, conversation_id="conversation-123"), + InferenceCallDetails(operationName=InferenceOperationType.CHAT, model="model", providerName="provider"), + AgentDetails("agent"), + ) as scope: + attrs = _span_attributes(scope) + assert attrs["gen_ai.conversation.id"] == "conversation-123" + if session_id is None: + assert "microsoft.session.id" not in attrs + else: + assert attrs["microsoft.session.id"] == session_id From b375b0edf58aec88f60eaf5e324fc33944539057 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Wed, 28 Oct 2026 16:03:22 -0600 Subject: [PATCH 2/3] Clarify session changelog and link Python PR Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfa03945-cb0e-4471-ba78-5980a3098d0b --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6c2217f2..841b854f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,8 +37,8 @@ - Log blob deletion-check filesystem errors without interrupting replay of other valid Agent365 records in the same batch. Verify deletion with `stat()` so Python 3.14 does not silently treat access failures as missing blobs. -- Propagate `Request.session_id` directly to Agent365 execute-tool and inference spans, - even without a baggage processor (microsoft/opentelemetry-distro-dotnet#116). +- Include the request's session ID on Agent365 tool and inference spans without requiring a baggage processor. + ([#310](https://github.com/microsoft/opentelemetry-distro-python/pull/310)) ### Other Changes - Update the `AZURE_OPENAI_API_VERSION` fallback version in integration tests From 84a2a127197aab3fb3a23f8fc3ce8842ecf934af Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Fri, 30 Oct 2026 10:41:39 -0600 Subject: [PATCH 3/3] Use Chat Completions for Agent Framework integration tests Select the released OpenAIChatCompletionClient instead of the Responses client and pass the configured Azure API version explicitly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 14661e89-86a3-4afe-a454-263eaeeba021 --- .../test_agentframework_trace_processor.py | 8 +++++--- .../integration/agentframework/test_message_format.py | 11 ++++++----- .../agentframework/test_observability_pipeline.py | 11 ++++++----- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/a365/integration/agentframework/test_agentframework_trace_processor.py b/tests/a365/integration/agentframework/test_agentframework_trace_processor.py index 9710d349b..403014782 100644 --- a/tests/a365/integration/agentframework/test_agentframework_trace_processor.py +++ b/tests/a365/integration/agentframework/test_agentframework_trace_processor.py @@ -19,7 +19,7 @@ try: from agent_framework._agents import RawAgent from agent_framework._tools import tool as ai_function - from agent_framework.openai import OpenAIChatClient + from agent_framework.openai import OpenAIChatCompletionClient except ImportError as _e: pytest.skip( f"AgentFramework library and dependencies required for integration tests: {_e}", @@ -47,10 +47,11 @@ def test_agentframework_trace_processor_integration(self, distro_exporter, azure """Test AgentFramework trace processor with real Azure OpenAI call.""" # Create Azure OpenAI ChatClient - chat_client = OpenAIChatClient( + chat_client = OpenAIChatCompletionClient( model=azure_openai_config["deployment"], azure_endpoint=azure_openai_config["endpoint"], api_key=azure_openai_config["api_key"], + api_version=azure_openai_config["api_version"], ) # Create agent framework agent @@ -93,10 +94,11 @@ def test_agentframework_trace_processor_with_tool_calls( """Test AgentFramework trace processor with tool calls.""" # Create Azure OpenAI ChatClient - chat_client = OpenAIChatClient( + chat_client = OpenAIChatCompletionClient( model=azure_openai_config["deployment"], azure_endpoint=azure_openai_config["endpoint"], api_key=azure_openai_config["api_key"], + api_version=azure_openai_config["api_version"], ) # Create agent framework agent diff --git a/tests/a365/integration/agentframework/test_message_format.py b/tests/a365/integration/agentframework/test_message_format.py index 8a27f49fe..d279a0cea 100644 --- a/tests/a365/integration/agentframework/test_message_format.py +++ b/tests/a365/integration/agentframework/test_message_format.py @@ -21,7 +21,7 @@ try: from agent_framework._agents import RawAgent from agent_framework._tools import tool - from agent_framework.openai import OpenAIChatClient + from agent_framework.openai import OpenAIChatCompletionClient except ImportError: pytest.skip( "AgentFramework library and dependencies required for integration tests", @@ -54,12 +54,13 @@ class TestAgentFrameworkMessageFormat: and verify the A365 structured message format.""" @pytest.fixture - def chat_client(self, azure_openai_config: dict[str, Any]) -> OpenAIChatClient: + def chat_client(self, azure_openai_config: dict[str, Any]) -> OpenAIChatCompletionClient: """Create a real Azure OpenAI chat client.""" - return OpenAIChatClient( + return OpenAIChatCompletionClient( model=azure_openai_config["deployment"], azure_endpoint=azure_openai_config["endpoint"], api_key=azure_openai_config["api_key"], + api_version=azure_openai_config["api_version"], ) def _find_chat_spans(self, distro_exporter) -> list[ReadableSpan]: @@ -72,7 +73,7 @@ def _find_chat_spans(self, distro_exporter) -> list[ReadableSpan]: return [s for s in distro_exporter.spans if s.attributes and GEN_AI_INPUT_MESSAGES_KEY in s.attributes] @pytest.mark.asyncio - async def test_simple_chat_message_mapping(self, distro_exporter, chat_client: OpenAIChatClient) -> None: + async def test_simple_chat_message_mapping(self, distro_exporter, chat_client: OpenAIChatCompletionClient) -> None: """Simple chat: verify exported spans contain structured A365 messages after enrichment (no manual mapper call).""" agent = RawAgent( @@ -124,7 +125,7 @@ async def test_simple_chat_message_mapping(self, distro_exporter, chat_client: O print(f"\n=== Enriched output ===\n{json.dumps(output_data, indent=2)}") @pytest.mark.asyncio - async def test_tool_call_message_mapping(self, distro_exporter, chat_client: OpenAIChatClient) -> None: + async def test_tool_call_message_mapping(self, distro_exporter, chat_client: OpenAIChatCompletionClient) -> None: """Tool-calling chat: verify tool_call and tool_call_response parts survive enrichment in exported spans.""" agent = RawAgent( diff --git a/tests/a365/integration/agentframework/test_observability_pipeline.py b/tests/a365/integration/agentframework/test_observability_pipeline.py index a00e17821..d6aa2a463 100644 --- a/tests/a365/integration/agentframework/test_observability_pipeline.py +++ b/tests/a365/integration/agentframework/test_observability_pipeline.py @@ -23,7 +23,7 @@ try: from agent_framework._agents import RawAgent from agent_framework._tools import tool as ai_function - from agent_framework.openai import OpenAIChatClient + from agent_framework.openai import OpenAIChatCompletionClient except ImportError: pytest.skip( "AgentFramework library and dependencies required for integration tests", @@ -87,12 +87,13 @@ class TestAgentFrameworkObservabilityPipeline: """ @pytest.fixture - def chat_client(self, azure_openai_config: dict) -> OpenAIChatClient: + def chat_client(self, azure_openai_config: dict) -> OpenAIChatCompletionClient: """Create a real Azure OpenAI chat client.""" - return OpenAIChatClient( + return OpenAIChatCompletionClient( model=azure_openai_config["deployment"], azure_endpoint=azure_openai_config["endpoint"], api_key=azure_openai_config["api_key"], + api_version=azure_openai_config["api_version"], ) @pytest.fixture @@ -119,7 +120,7 @@ def _flush_and_collect(self, distro_exporter) -> list[ReadableSpan]: async def test_pipeline_invoke_agent_with_tool_call( # pylint: disable=too-many-statements self, distro_exporter, - chat_client: OpenAIChatClient, + chat_client: OpenAIChatCompletionClient, agent_details: AgentDetails, agent365_config: dict, ) -> None: @@ -262,7 +263,7 @@ async def test_pipeline_invoke_agent_with_tool_call( # pylint: disable=too-many async def test_pipeline_invoke_agent_simple_inference( self, distro_exporter, - chat_client: OpenAIChatClient, + chat_client: OpenAIChatCompletionClient, agent_details: AgentDetails, agent365_config: dict, ) -> None: