Skip to content

Commit 233a0fb

Browse files
Merge branch 'copilot/implement-baggage-middleware-python' of https://github.com/microsoft/Agent365-python into copilot/implement-baggage-middleware-python
2 parents 734ec1a + 480e469 commit 233a0fb

File tree

5 files changed

+46
-102
lines changed

5 files changed

+46
-102
lines changed

libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/middleware/baggage_middleware.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from __future__ import annotations
77

8-
import logging
98
from collections.abc import Awaitable, Callable
109

1110
from microsoft_agents.activity import ActivityEventNames, ActivityTypes
@@ -14,8 +13,6 @@
1413

1514
from ..scope_helpers.populate_baggage import populate
1615

17-
logger = logging.getLogger(__name__)
18-
1916

2017
class BaggageMiddleware:
2118
"""Middleware that propagates OpenTelemetry baggage context derived from TurnContext.

libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/middleware/output_logging_middleware.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,15 @@
4242

4343

4444
def _derive_agent_details(context: TurnContext) -> AgentDetails | None:
45-
"""Derive target agent details from the activity recipient."""
45+
"""Derive target agent details from the activity recipient.
46+
47+
Returns ``None`` when the activity is not an agentic request or the
48+
recipient is missing, so callers can short-circuit without emitting
49+
spans with empty identifiers.
50+
"""
4651
activity = context.activity
52+
if not activity.is_agentic_request():
53+
return None
4754
recipient = getattr(activity, "recipient", None)
4855
if not recipient:
4956
return None

tests/observability/hosting/middleware/test_baggage_middleware.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33

4-
from unittest.mock import AsyncMock, MagicMock
4+
from unittest.mock import MagicMock
55

66
import pytest
77
from microsoft_agents.activity import (
@@ -95,15 +95,3 @@ 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_calls_logic():
102-
"""BaggageMiddleware should always call the downstream logic."""
103-
middleware = BaggageMiddleware()
104-
ctx = _make_turn_context()
105-
106-
logic_mock = AsyncMock()
107-
await middleware.on_turn(ctx, logic_mock)
108-
109-
logic_mock.assert_awaited_once()

tests/observability/hosting/middleware/test_observability_hosting_manager.py

Lines changed: 34 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -24,73 +24,50 @@ def _reset_singleton():
2424
ObservabilityHostingManager.reset()
2525

2626

27-
def test_configure_returns_instance():
28-
"""configure() should return an ObservabilityHostingManager instance."""
29-
middleware_set = MagicMock()
30-
instance = ObservabilityHostingManager.configure(middleware_set, ObservabilityHostingOptions())
31-
assert isinstance(instance, ObservabilityHostingManager)
32-
33-
3427
def test_configure_is_singleton():
35-
"""Subsequent configure() calls should return the same instance."""
28+
"""configure() should return an ObservabilityHostingManager and be a singleton."""
3629
middleware_set = MagicMock()
3730
options = ObservabilityHostingOptions()
3831
first = ObservabilityHostingManager.configure(middleware_set, options)
32+
assert isinstance(first, ObservabilityHostingManager)
3933
second = ObservabilityHostingManager.configure(middleware_set, options)
4034
assert first is second
4135

4236

43-
def test_configure_registers_baggage_middleware_by_default():
44-
"""By default, BaggageMiddleware should be registered."""
45-
middleware_set = MagicMock()
46-
ObservabilityHostingManager.configure(middleware_set, ObservabilityHostingOptions())
47-
48-
# The middleware_set.use should have been called once (only BaggageMiddleware by default)
49-
assert middleware_set.use.call_count == 1
50-
registered = middleware_set.use.call_args_list[0][0][0]
51-
assert isinstance(registered, BaggageMiddleware)
52-
53-
54-
def test_configure_registers_both_middlewares():
55-
"""When output logging is enabled, both middlewares should be registered."""
56-
middleware_set = MagicMock()
57-
options = ObservabilityHostingOptions(enable_baggage=True, enable_output_logging=True)
58-
ObservabilityHostingManager.configure(middleware_set, options)
59-
60-
assert middleware_set.use.call_count == 2
61-
registered_types = [c[0][0] for c in middleware_set.use.call_args_list]
62-
assert isinstance(registered_types[0], BaggageMiddleware)
63-
assert isinstance(registered_types[1], OutputLoggingMiddleware)
64-
65-
66-
def test_configure_disables_baggage():
67-
"""When baggage is disabled, only output logging should be registered (if enabled)."""
68-
middleware_set = MagicMock()
69-
options = ObservabilityHostingOptions(enable_baggage=False, enable_output_logging=True)
70-
ObservabilityHostingManager.configure(middleware_set, options)
71-
72-
assert middleware_set.use.call_count == 1
73-
registered = middleware_set.use.call_args_list[0][0][0]
74-
assert isinstance(registered, OutputLoggingMiddleware)
75-
76-
77-
def test_configure_disables_all():
78-
"""When both are disabled, no middleware should be registered."""
37+
@pytest.mark.parametrize(
38+
"enable_baggage,enable_output_logging,expected_types",
39+
[
40+
(True, False, [BaggageMiddleware]),
41+
(True, True, [BaggageMiddleware, OutputLoggingMiddleware]),
42+
(False, True, [OutputLoggingMiddleware]),
43+
(False, False, []),
44+
],
45+
ids=["default_baggage_only", "both_enabled", "output_only", "none"],
46+
)
47+
def test_configure_registers_expected_middlewares(
48+
enable_baggage, enable_output_logging, expected_types
49+
):
50+
"""configure() should register the correct middlewares based on options."""
7951
middleware_set = MagicMock()
80-
options = ObservabilityHostingOptions(enable_baggage=False, enable_output_logging=False)
52+
options = ObservabilityHostingOptions(
53+
enable_baggage=enable_baggage, enable_output_logging=enable_output_logging
54+
)
8155
ObservabilityHostingManager.configure(middleware_set, options)
8256

83-
middleware_set.use.assert_not_called()
84-
85-
86-
def test_configure_raises_on_none_middleware_set():
87-
"""configure() should raise TypeError when middleware_set is None."""
88-
with pytest.raises(TypeError, match="middleware_set must not be None"):
89-
ObservabilityHostingManager.configure(None, ObservabilityHostingOptions())
57+
assert middleware_set.use.call_count == len(expected_types)
58+
for call, expected_type in zip(middleware_set.use.call_args_list, expected_types, strict=True):
59+
assert isinstance(call[0][0], expected_type)
9060

9161

92-
def test_configure_raises_on_none_options():
93-
"""configure() should raise TypeError when options is None."""
94-
middleware_set = MagicMock()
95-
with pytest.raises(TypeError, match="options must not be None"):
96-
ObservabilityHostingManager.configure(middleware_set, None)
62+
@pytest.mark.parametrize(
63+
"middleware_set,options,match",
64+
[
65+
(None, ObservabilityHostingOptions(), "middleware_set must not be None"),
66+
(MagicMock(), None, "options must not be None"),
67+
],
68+
ids=["none_middleware_set", "none_options"],
69+
)
70+
def test_configure_raises_on_none(middleware_set, options, match):
71+
"""configure() should raise TypeError when required args are None."""
72+
with pytest.raises(TypeError, match=match):
73+
ObservabilityHostingManager.configure(middleware_set, options)

tests/observability/hosting/middleware/test_output_logging_middleware.py

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def _make_turn_context(
3636
),
3737
"recipient": ChannelAccount(
3838
tenant_id=recipient_tenant_id,
39-
role="assistant",
39+
role="agenticAppInstance",
4040
name="Agent One",
4141
agentic_app_id=recipient_agentic_app_id,
4242
aad_object_id="agent-auid",
@@ -136,7 +136,7 @@ async def test_send_handler_skips_non_message_activities():
136136

137137
@pytest.mark.asyncio
138138
async def test_send_handler_creates_output_scope_for_messages():
139-
"""Send handler should create an OutputScope for message activities."""
139+
"""Send handler should create an OutputScope for message activities and dispose on success."""
140140
middleware = OutputLoggingMiddleware()
141141
ctx = _make_turn_context()
142142

@@ -159,6 +159,7 @@ async def test_send_handler_creates_output_scope_for_messages():
159159
mock_output_scope_cls.start.assert_called_once()
160160
send_next.assert_awaited_once()
161161
mock_scope.dispose.assert_called_once()
162+
mock_scope.record_error.assert_not_called()
162163

163164

164165
@pytest.mark.asyncio
@@ -216,29 +217,3 @@ async def test_send_handler_rethrows_errors():
216217

217218
mock_scope.record_error.assert_called_once_with(send_error)
218219
mock_scope.dispose.assert_called_once()
219-
220-
221-
@pytest.mark.asyncio
222-
async def test_send_handler_disposes_scope_on_success():
223-
"""Send handler should dispose the OutputScope even when send_next succeeds."""
224-
middleware = OutputLoggingMiddleware()
225-
ctx = _make_turn_context()
226-
227-
await middleware.on_turn(ctx, AsyncMock())
228-
229-
handler = ctx._on_send_activities[-1]
230-
231-
activities = [Activity(type="message", text="Reply")]
232-
send_next = AsyncMock()
233-
234-
with patch(
235-
"microsoft_agents_a365.observability.hosting.middleware"
236-
".output_logging_middleware.OutputScope"
237-
) as mock_output_scope_cls:
238-
mock_scope = MagicMock()
239-
mock_output_scope_cls.start.return_value = mock_scope
240-
241-
await handler(ctx, activities, send_next)
242-
243-
mock_scope.dispose.assert_called_once()
244-
mock_scope.record_error.assert_not_called()

0 commit comments

Comments
 (0)