Skip to content

Commit ecd35bb

Browse files
author
Grant Harris
committed
add product context fallback
1 parent 86e70b2 commit ecd35bb

2 files changed

Lines changed: 108 additions & 4 deletions

File tree

  • libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers
  • tests/observability/hosting/middleware

libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/scope_helpers/utils.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,36 @@ def get_channel_pairs(activity: Activity) -> Iterator[tuple[str, Any]]:
7474
sub_channel = None
7575

7676
if channel_id is not None:
77-
if isinstance(channel_id, str):
78-
# Direct string value
79-
channel_name = channel_id
80-
elif hasattr(channel_id, "channel"):
77+
# Check for ChannelId object first
78+
if hasattr(channel_id, "channel"):
8179
# ChannelId object
8280
channel_name = channel_id.channel
8381
sub_channel = channel_id.sub_channel
82+
elif isinstance(channel_id, str):
83+
# Direct string value
84+
channel_name = channel_id
85+
86+
# Try to get sub_channel from productContext in channel_data if sub_channel is not set
87+
if not sub_channel and activity.channel_data:
88+
try:
89+
import json
90+
# Convert channel_data to dict if it's a string
91+
if isinstance(activity.channel_data, str):
92+
channel_data_dict = json.loads(activity.channel_data)
93+
elif isinstance(activity.channel_data, dict):
94+
channel_data_dict = activity.channel_data
95+
else:
96+
# Try to convert to dict if it has __dict__
97+
channel_data_dict = getattr(activity.channel_data, '__dict__', {})
98+
99+
# Extract productContext if available
100+
if isinstance(channel_data_dict, dict) and 'productContext' in channel_data_dict:
101+
product_context = channel_data_dict['productContext']
102+
if isinstance(product_context, str):
103+
sub_channel = product_context
104+
except (json.JSONDecodeError, AttributeError, TypeError):
105+
# Silently ignore any parsing errors
106+
pass
84107

85108
# Yield channel name as source name
86109
yield CHANNEL_NAME_KEY, channel_name

tests/observability/hosting/middleware/test_baggage_middleware.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,84 @@ async def logic():
9595
assert logic_called is True
9696
# Baggage should NOT be set because the middleware skipped it
9797
assert captured_caller_id is None
98+
99+
100+
@pytest.mark.asyncio
101+
async def test_baggage_middleware_extracts_product_context_from_channel_data():
102+
"""BaggageMiddleware should extract productContext from channel_data when sub_channel is not set."""
103+
from microsoft_agents.activity import ChannelId
104+
from microsoft_agents_a365.observability.core.constants import CHANNEL_LINK_KEY
105+
106+
middleware = BaggageMiddleware()
107+
108+
# Create activity with ChannelId (no sub_channel) and channel_data with productContext
109+
activity = Activity(
110+
type="message",
111+
text="Hello",
112+
from_property=ChannelAccount(
113+
aad_object_id="caller-id",
114+
name="Caller",
115+
),
116+
recipient=ChannelAccount(
117+
tenant_id="tenant-123",
118+
name="Agent",
119+
),
120+
conversation=ConversationAccount(id="conv-id"),
121+
service_url="https://example.com",
122+
channel_id=ChannelId(channel="msteams"), # No sub_channel
123+
channel_data={"productContext": "COPILOT"},
124+
)
125+
126+
adapter = MagicMock()
127+
ctx = TurnContext(adapter, activity)
128+
129+
captured_channel_link = None
130+
131+
async def logic():
132+
nonlocal captured_channel_link
133+
captured_channel_link = baggage.get_baggage(CHANNEL_LINK_KEY)
134+
135+
await middleware.on_turn(ctx, logic)
136+
137+
assert captured_channel_link == "COPILOT"
138+
139+
140+
@pytest.mark.asyncio
141+
async def test_baggage_middleware_sub_channel_takes_precedence_over_product_context():
142+
"""BaggageMiddleware should use sub_channel when both sub_channel and productContext are present."""
143+
from microsoft_agents.activity import ChannelId
144+
from microsoft_agents_a365.observability.core.constants import CHANNEL_LINK_KEY
145+
146+
middleware = BaggageMiddleware()
147+
148+
# Create activity with BOTH sub_channel and productContext in channel_data
149+
activity = Activity(
150+
type="message",
151+
text="Hello",
152+
from_property=ChannelAccount(
153+
aad_object_id="caller-id",
154+
name="Caller",
155+
),
156+
recipient=ChannelAccount(
157+
tenant_id="tenant-123",
158+
name="Agent",
159+
),
160+
conversation=ConversationAccount(id="conv-id"),
161+
service_url="https://example.com",
162+
channel_id=ChannelId(channel="msteams", sub_channel="teams-subchannel"),
163+
channel_data={"productContext": "COPILOT"}, # Should be ignored
164+
)
165+
166+
adapter = MagicMock()
167+
ctx = TurnContext(adapter, activity)
168+
169+
captured_channel_link = None
170+
171+
async def logic():
172+
nonlocal captured_channel_link
173+
captured_channel_link = baggage.get_baggage(CHANNEL_LINK_KEY)
174+
175+
await middleware.on_turn(ctx, logic)
176+
177+
# sub_channel should take precedence, productContext should be ignored
178+
assert captured_channel_link == "teams-subchannel"

0 commit comments

Comments
 (0)