From a1f388bc038af5a434ef68c10cd02883cf63c037 Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Mon, 4 May 2026 17:53:52 -0700 Subject: [PATCH 1/3] Fix GetCallerBaggagePairs: resolve userId across all channels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit userId was only set from aad_object_id, which is None on non-Teams channels and A2A calls. Add fallback chain: aad_object_id → agentic_user_id → frm.id Port of microsoft/Agent365-dotnet#246 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../hosting/scope_helpers/utils.py | 5 +- .../scope_helpers/test_scope_helper_utils.py | 47 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py index 737f57f0..63392a16 100644 --- a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py +++ b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py @@ -36,7 +36,10 @@ def get_caller_pairs(activity: Activity) -> Iterator[tuple[str, Any]]: frm = activity.from_property if not frm: return - yield USER_ID_KEY, frm.aad_object_id + # Fallback chain for user_id: AadObjectId → AgenticUserId → From.Id + # AadObjectId is null on non-Teams channels and A2A calls (see dotnet PR #246) + user_id = frm.aad_object_id or frm.agentic_user_id or frm.id + yield USER_ID_KEY, user_id yield USER_NAME_KEY, frm.name yield USER_EMAIL_KEY, frm.agentic_user_id diff --git a/tests/observability/hosting/scope_helpers/test_scope_helper_utils.py b/tests/observability/hosting/scope_helpers/test_scope_helper_utils.py index 60e4327f..d5baa2c6 100644 --- a/tests/observability/hosting/scope_helpers/test_scope_helper_utils.py +++ b/tests/observability/hosting/scope_helpers/test_scope_helper_utils.py @@ -83,6 +83,53 @@ def test_get_channel_pairs(): assert (CHANNEL_LINK_KEY, None) in result +def test_get_caller_pairs_non_teams_fallback_to_from_id(): + """Test userId falls back to from.id when aad_object_id is None (non-Teams channel).""" + from_account = ChannelAccount( + id="from-id-123", + name="Non-Teams User", + ) + activity = Activity(type="message", from_property=from_account) + + result = list(get_caller_pairs(activity)) + + assert (USER_ID_KEY, "from-id-123") in result + assert (USER_NAME_KEY, "Non-Teams User") in result + assert (USER_EMAIL_KEY, None) in result + + +def test_get_caller_pairs_a2a_fallback_to_agentic_user_id(): + """Test userId falls back to agentic_user_id for A2A calls (no aad_object_id).""" + from_account = ChannelAccount( + id="from-id-456", + name="Agent Caller", + agentic_user_id="a2a-agent-guid", + ) + activity = Activity(type="message", from_property=from_account) + + result = list(get_caller_pairs(activity)) + + assert (USER_ID_KEY, "a2a-agent-guid") in result + assert (USER_EMAIL_KEY, "a2a-agent-guid") in result + + +def test_get_caller_pairs_aad_object_id_wins_when_all_set(): + """Test aad_object_id takes precedence when all identifiers are present.""" + from_account = ChannelAccount( + id="from-id-789", + aad_object_id="aad-wins", + name="Full User", + agentic_user_id="agent-upn", + ) + activity = Activity(type="message", from_property=from_account) + + result = list(get_caller_pairs(activity)) + + assert (USER_ID_KEY, "aad-wins") in result + assert (USER_NAME_KEY, "Full User") in result + assert (USER_EMAIL_KEY, "agent-upn") in result + + def test_get_conversation_pairs(): """Test get_conversation_pairs extracts conversation information.""" conversation = ConversationAccount(id="conversation-123") From 1b9204186bf94b4419089bc05f6f3335ae7d0d56 Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Mon, 4 May 2026 18:32:26 -0700 Subject: [PATCH 2/3] Address Copilot review feedback: clean up misleading comments Co-Authored-By: Claude Opus 4.6 (1M context) --- .../observability/hosting/scope_helpers/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py index 63392a16..5e97c004 100644 --- a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py +++ b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py @@ -37,7 +37,7 @@ def get_caller_pairs(activity: Activity) -> Iterator[tuple[str, Any]]: if not frm: return # Fallback chain for user_id: AadObjectId → AgenticUserId → From.Id - # AadObjectId is null on non-Teams channels and A2A calls (see dotnet PR #246) + # AadObjectId is null on non-Teams channels and A2A calls user_id = frm.aad_object_id or frm.agentic_user_id or frm.id yield USER_ID_KEY, user_id yield USER_NAME_KEY, frm.name From c8532916526e736c85a0015a8e79a0ac95b8aa8f Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Mon, 4 May 2026 18:54:59 -0700 Subject: [PATCH 3/3] Address review: inline fallback, add GUID test to match .NET PR #246 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../observability/hosting/scope_helpers/utils.py | 5 +---- .../hosting/scope_helpers/test_scope_helper_utils.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py index 5e97c004..298294c1 100644 --- a/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py +++ b/libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py @@ -36,10 +36,7 @@ def get_caller_pairs(activity: Activity) -> Iterator[tuple[str, Any]]: frm = activity.from_property if not frm: return - # Fallback chain for user_id: AadObjectId → AgenticUserId → From.Id - # AadObjectId is null on non-Teams channels and A2A calls - user_id = frm.aad_object_id or frm.agentic_user_id or frm.id - yield USER_ID_KEY, user_id + yield USER_ID_KEY, frm.aad_object_id or frm.agentic_user_id or frm.id yield USER_NAME_KEY, frm.name yield USER_EMAIL_KEY, frm.agentic_user_id diff --git a/tests/observability/hosting/scope_helpers/test_scope_helper_utils.py b/tests/observability/hosting/scope_helpers/test_scope_helper_utils.py index d5baa2c6..15348e82 100644 --- a/tests/observability/hosting/scope_helpers/test_scope_helper_utils.py +++ b/tests/observability/hosting/scope_helpers/test_scope_helper_utils.py @@ -130,6 +130,18 @@ def test_get_caller_pairs_aad_object_id_wins_when_all_set(): assert (USER_EMAIL_KEY, "agent-upn") in result +def test_get_caller_pairs_a2a_guid_agentic_user_id(): + """Test userId resolves to GUID AgenticUserId in A2A scenario.""" + from_account = ChannelAccount( + id="29:1sH5NArUwkWAX", + name="Agent Caller", + agentic_user_id="bef730f4-d6f5-4ffb-b759-26ffa449ed7e", + ) + activity = Activity(type="message", from_property=from_account) + result = list(get_caller_pairs(activity)) + assert (USER_ID_KEY, "bef730f4-d6f5-4ffb-b759-26ffa449ed7e") in result + + def test_get_conversation_pairs(): """Test get_conversation_pairs extracts conversation information.""" conversation = ConversationAccount(id="conversation-123")