diff --git a/A365_DOCUMENTATION.md b/A365_DOCUMENTATION.md index 023d7530a..3b6c8d376 100644 --- a/A365_DOCUMENTATION.md +++ b/A365_DOCUMENTATION.md @@ -552,6 +552,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 edfb8cea4..0ee1f288f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +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. +- 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)) ### Features Added - Record full endpoint URLs on Agent365 tool spans with `ServiceEndpoint.from_url()`. diff --git a/src/microsoft/opentelemetry/a365/core/execute_tool_scope.py b/src/microsoft/opentelemetry/a365/core/execute_tool_scope.py index b2f19b390..a65ef9480 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, URL_FULL_KEY, USER_EMAIL_KEY, USER_ID_KEY, @@ -147,6 +148,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 340f87ecb..821871c13 100644 --- a/tests/a365/test_execute_tool_scope.py +++ b/tests/a365/test_execute_tool_scope.py @@ -262,6 +262,27 @@ def test_unserializable_typed_payloads_are_safe_when_telemetry_is_disabled(self) 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() + + @pytest.mark.parametrize( "url,host,port", [ 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