From 7a2d38bca308d5bc13f308297048bb5a7421c301 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 25 Nov 2025 08:31:04 -0800 Subject: [PATCH 1/7] Allowing null value for service_url --- .../microsoft_agents/activity/activity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index 5ea03b26..6b0a8348 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -525,7 +525,7 @@ def create_reply(self, text: str = None, locale: str = None): or self.channel_id not in ["directline", "webchat"] else None ), - service_url=self.service_url, + service_url=SkipNone(self.service_url), channel_id=self.channel_id, conversation=SkipNone( ConversationAccount.pick_properties( From 5cdf81da099213a7db57e9ab370f91a3f1fc3eef Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 25 Nov 2025 09:02:33 -0800 Subject: [PATCH 2/7] Adding check for necessity of ConnectorClient creation --- .../hosting/core/channel_service_adapter.py | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index 5eed20d0..5d59f411 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -341,6 +341,17 @@ async def process_proactive( await connector_client.close() await user_token_client.close() + def _resolve_if_connector_client_is_needed(self, activity: Activity) -> bool: + """Determine if a connector client is needed based on the activity's delivery mode and service URL. + + :param activity: The activity to evaluate. + :type activity: :class:`microsoft_agents.activity.Activity` + """ + if activity.delivery_mode in [DeliveryModes.expect_replies, DeliveryModes.stream]: + if not activity.service_url: + return False + return True + async def process_activity( self, claims_identity: ClaimsIdentity, @@ -403,21 +414,24 @@ async def process_activity( context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client # Create the connector client to use for outbound requests. - connector_client: ConnectorClient = ( - await self._channel_service_client_factory.create_connector_client( - context, - claims_identity, - activity.service_url, - outgoing_audience, - scopes, - use_anonymous_auth_callback, + connector_client: Optional[ConnectorClient] = None + if self._resolve_if_connector_client_is_needed(activity): + connector_client = ( + await self._channel_service_client_factory.create_connector_client( + context, + claims_identity, + activity.service_url, + outgoing_audience, + scopes, + use_anonymous_auth_callback, + ) ) - ) - context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client + context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client await self.run_pipeline(context, callback) - await connector_client.close() + if connector_client: + await connector_client.close() await user_token_client.close() # If there are any results they will have been left on the TurnContext. From 05a8b5a5dc8ff73cd8429595cd936e9f34abf946 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 25 Nov 2025 09:04:41 -0800 Subject: [PATCH 3/7] Adding comment --- .../hosting/core/channel_service_adapter.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index 5d59f411..31874dbb 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -343,12 +343,15 @@ async def process_proactive( def _resolve_if_connector_client_is_needed(self, activity: Activity) -> bool: """Determine if a connector client is needed based on the activity's delivery mode and service URL. - + :param activity: The activity to evaluate. :type activity: :class:`microsoft_agents.activity.Activity` """ - if activity.delivery_mode in [DeliveryModes.expect_replies, DeliveryModes.stream]: - if not activity.service_url: + if activity.delivery_mode in [ + DeliveryModes.expect_replies, + DeliveryModes.stream, + ]: + if not activity.service_url: # this is breaking... return False return True From d32a46fd63e76cb966d36f1e105ed4c02a76d730 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 25 Nov 2025 09:14:48 -0800 Subject: [PATCH 4/7] Undoing change in Activity and reformatting --- .../microsoft_agents/activity/activity.py | 2 +- .../microsoft_agents/hosting/core/channel_service_adapter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index 6b0a8348..5ea03b26 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -525,7 +525,7 @@ def create_reply(self, text: str = None, locale: str = None): or self.channel_id not in ["directline", "webchat"] else None ), - service_url=SkipNone(self.service_url), + service_url=self.service_url, channel_id=self.channel_id, conversation=SkipNone( ConversationAccount.pick_properties( diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index 31874dbb..bb73abe9 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -351,7 +351,7 @@ def _resolve_if_connector_client_is_needed(self, activity: Activity) -> bool: DeliveryModes.expect_replies, DeliveryModes.stream, ]: - if not activity.service_url: # this is breaking... + if not activity.service_url: # this is breaking... return False return True From d3d44d4fb5fb632af95b2fc455440b1d8ec4e863 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 25 Nov 2025 09:33:51 -0800 Subject: [PATCH 5/7] Adding process_proactive and process_activity tests for ChannelServiceAdapter --- .../test_channel_service_adapter.py | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py index 777f7cef..e5ce2df1 100644 --- a/tests/hosting_core/test_channel_service_adapter.py +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -1,8 +1,10 @@ import pytest from microsoft_agents.activity import ( + Activity, ConversationResourceResponse, ConversationParameters, + DeliveryModes, ) from microsoft_agents.hosting.core import ( ChannelServiceAdapter, @@ -14,6 +16,7 @@ TeamsConnectorClient, UserTokenClient, Connections, + ClaimsIdentity, ) from microsoft_agents.hosting.core.connector.conversations_base import ConversationsBase @@ -94,3 +97,183 @@ async def callback(context: TurnContext): assert context_arg.activity.conversation.id == "conversation123" assert context_arg.activity.channel_id == "channel_id" assert context_arg.activity.service_url == "service_url" + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "delivery_mode", + [DeliveryModes.expect_replies, DeliveryModes.stream], + ) + async def test_process_activity_expect_replies_and_stream_without_service_url( + self, mocker, user_token_client, adapter, delivery_mode + ): + user_token_client.get_access_token = mocker.AsyncMock( + return_value="user_token_value" + ) + adapter.run_pipeline = mocker.AsyncMock() + + async def callback(context: TurnContext): + return None + + activity = Activity( # type: ignore + type="message", + conversation={"id": "conversation123"}, + channel_id="channel_id", + delivery_mode=delivery_mode + ) + + claims_identity = ClaimsIdentity( + { + "aud": "agent_app_id", + "ver": "2.0", + "azp": "outgoing_app_id", + }, + is_authenticated=True, + ) + + await adapter.process_activity( + claims_identity, + activity, + callback, + ) + + adapter.run_pipeline.assert_awaited_once() + + context_arg, callback_arg = adapter.run_pipeline.call_args[0] + assert callback_arg == callback + assert context_arg.activity == activity + + assert context_arg.activity.conversation.id == "conversation123" + assert context_arg.activity.channel_id == "channel_id" + assert context_arg.activity.service_url is None + assert context_arg.turn_state[ChannelServiceAdapter.USER_TOKEN_CLIENT_KEY] is user_token_client + assert ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY not in context_arg.turn_state + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "delivery_mode", + [DeliveryModes.expect_replies, DeliveryModes.stream], + ) + async def test_process_activity_expect_replies_and_stream_with_service_url( + self, mocker, user_token_client, connector_client, adapter, delivery_mode + ): + user_token_client.get_access_token = mocker.AsyncMock( + return_value="user_token_value" + ) + adapter.run_pipeline = mocker.AsyncMock() + + async def callback(context: TurnContext): + return None + + activity = Activity( # type: ignore + type="message", + conversation={"id": "conversation123"}, + channel_id="channel_id", + delivery_mode=delivery_mode, + service_url="service_url", + ) + + claims_identity = ClaimsIdentity( + { + "aud": "agent_app_id", + "ver": "2.0", + "azp": "outgoing_app_id", + }, + is_authenticated=True, + ) + + await adapter.process_activity( + claims_identity, + activity, + callback, + ) + + adapter.run_pipeline.assert_awaited_once() + + context_arg, callback_arg = adapter.run_pipeline.call_args[0] + assert callback_arg == callback + assert context_arg.activity == activity + + assert context_arg.activity.conversation.id == "conversation123" + assert context_arg.activity.channel_id == "channel_id" + assert context_arg.activity.service_url == "service_url" + assert context_arg.turn_state[ChannelServiceAdapter.USER_TOKEN_CLIENT_KEY] is user_token_client + assert context_arg.turn_state[ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY] is connector_client + + @pytest.mark.asyncio + async def test_process_activity_normal_no_service_url( + self, mocker, user_token_client, adapter + ): + user_token_client.get_access_token = mocker.AsyncMock( + return_value="user_token_value" + ) + adapter.run_pipeline = mocker.AsyncMock() + + async def callback(context: TurnContext): + return None + + activity = Activity( # type: ignore + type="message", + conversation={"id": "conversation123"}, + channel_id="channel_id", + ) + + claims_identity = ClaimsIdentity( + { + "aud": "agent_app_id", + "ver": "2.0", + "azp": "outgoing_app_id", + }, + is_authenticated=True, + ) + + with pytest.raises(Exception) as exc_info: + await adapter.process_activity( + claims_identity, + activity, + callback, + ) + + @pytest.mark.asyncio + async def test_process_proactive(self, mocker, user_token_client, connector_client, adapter): + user_token_client.get_access_token = mocker.AsyncMock( + return_value="user_token_value" + ) + adapter.run_pipeline = mocker.AsyncMock() + + async def callback(context: TurnContext): + return None + + activity = Activity( # type: ignore + type="message", + conversation={"id": "conversation123"}, + channel_id="channel_id", + service_url="service_url", + ) + + claims_identity = ClaimsIdentity( + { + "aud": "agent_app_id", + "ver": "2.0", + "azp": "outgoing_app_id", + }, + is_authenticated=True, + ) + + await adapter.process_proactive( + claims_identity, + activity, + "audience", + callback, + ) + + adapter.run_pipeline.assert_awaited_once() + + context_arg, callback_arg = adapter.run_pipeline.call_args[0] + assert callback_arg == callback + assert context_arg.activity == activity + + assert context_arg.activity.conversation.id == "conversation123" + assert context_arg.activity.channel_id == "channel_id" + assert context_arg.activity.service_url == "service_url" + assert context_arg.turn_state[ChannelServiceAdapter.USER_TOKEN_CLIENT_KEY] is user_token_client + assert context_arg.turn_state[ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY] is connector_client \ No newline at end of file From 21134b8d14f3f16a418ab7fd8b3246650f83d96a Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 25 Nov 2025 10:19:34 -0800 Subject: [PATCH 6/7] Another commit --- .../test_channel_service_adapter.py | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py index e5ce2df1..60862d3e 100644 --- a/tests/hosting_core/test_channel_service_adapter.py +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -102,7 +102,7 @@ async def callback(context: TurnContext): @pytest.mark.parametrize( "delivery_mode", [DeliveryModes.expect_replies, DeliveryModes.stream], - ) + ) async def test_process_activity_expect_replies_and_stream_without_service_url( self, mocker, user_token_client, adapter, delivery_mode ): @@ -118,7 +118,7 @@ async def callback(context: TurnContext): type="message", conversation={"id": "conversation123"}, channel_id="channel_id", - delivery_mode=delivery_mode + delivery_mode=delivery_mode, ) claims_identity = ClaimsIdentity( @@ -145,14 +145,20 @@ async def callback(context: TurnContext): assert context_arg.activity.conversation.id == "conversation123" assert context_arg.activity.channel_id == "channel_id" assert context_arg.activity.service_url is None - assert context_arg.turn_state[ChannelServiceAdapter.USER_TOKEN_CLIENT_KEY] is user_token_client - assert ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY not in context_arg.turn_state + assert ( + context_arg.turn_state[ChannelServiceAdapter.USER_TOKEN_CLIENT_KEY] + is user_token_client + ) + assert ( + ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY + not in context_arg.turn_state + ) @pytest.mark.asyncio @pytest.mark.parametrize( "delivery_mode", [DeliveryModes.expect_replies, DeliveryModes.stream], - ) + ) async def test_process_activity_expect_replies_and_stream_with_service_url( self, mocker, user_token_client, connector_client, adapter, delivery_mode ): @@ -196,8 +202,14 @@ async def callback(context: TurnContext): assert context_arg.activity.conversation.id == "conversation123" assert context_arg.activity.channel_id == "channel_id" assert context_arg.activity.service_url == "service_url" - assert context_arg.turn_state[ChannelServiceAdapter.USER_TOKEN_CLIENT_KEY] is user_token_client - assert context_arg.turn_state[ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY] is connector_client + assert ( + context_arg.turn_state[ChannelServiceAdapter.USER_TOKEN_CLIENT_KEY] + is user_token_client + ) + assert ( + context_arg.turn_state[ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY] + is connector_client + ) @pytest.mark.asyncio async def test_process_activity_normal_no_service_url( @@ -234,7 +246,9 @@ async def callback(context: TurnContext): ) @pytest.mark.asyncio - async def test_process_proactive(self, mocker, user_token_client, connector_client, adapter): + async def test_process_proactive( + self, mocker, user_token_client, connector_client, adapter + ): user_token_client.get_access_token = mocker.AsyncMock( return_value="user_token_value" ) @@ -275,5 +289,11 @@ async def callback(context: TurnContext): assert context_arg.activity.conversation.id == "conversation123" assert context_arg.activity.channel_id == "channel_id" assert context_arg.activity.service_url == "service_url" - assert context_arg.turn_state[ChannelServiceAdapter.USER_TOKEN_CLIENT_KEY] is user_token_client - assert context_arg.turn_state[ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY] is connector_client \ No newline at end of file + assert ( + context_arg.turn_state[ChannelServiceAdapter.USER_TOKEN_CLIENT_KEY] + is user_token_client + ) + assert ( + context_arg.turn_state[ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY] + is connector_client + ) From 32e0b70b7ebe25883aa9b995a19216d4aa0f6fca Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 26 Nov 2025 12:05:16 -0800 Subject: [PATCH 7/7] Removed check for service_url when resolving whether to create ConnectorClient --- .../hosting/core/channel_service_adapter.py | 3 +- .../test_channel_service_adapter.py | 73 +++---------------- 2 files changed, 12 insertions(+), 64 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index bb73abe9..33fd7aa0 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -351,8 +351,7 @@ def _resolve_if_connector_client_is_needed(self, activity: Activity) -> bool: DeliveryModes.expect_replies, DeliveryModes.stream, ]: - if not activity.service_url: # this is breaking... - return False + return False return True async def process_activity( diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py index 60862d3e..ccf32b9e 100644 --- a/tests/hosting_core/test_channel_service_adapter.py +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -100,11 +100,16 @@ async def callback(context: TurnContext): @pytest.mark.asyncio @pytest.mark.parametrize( - "delivery_mode", - [DeliveryModes.expect_replies, DeliveryModes.stream], + "delivery_mode, service_url", + [ + [DeliveryModes.expect_replies, None], + [DeliveryModes.stream, None], + [DeliveryModes.expect_replies, "https://service.url"], + [DeliveryModes.stream, "https://service.url"], + ], ) - async def test_process_activity_expect_replies_and_stream_without_service_url( - self, mocker, user_token_client, adapter, delivery_mode + async def test_process_activity_expect_replies_and_stream( + self, mocker, user_token_client, adapter, delivery_mode, service_url ): user_token_client.get_access_token = mocker.AsyncMock( return_value="user_token_value" @@ -120,6 +125,7 @@ async def callback(context: TurnContext): channel_id="channel_id", delivery_mode=delivery_mode, ) + activity.service_url = service_url claims_identity = ClaimsIdentity( { @@ -144,7 +150,7 @@ async def callback(context: TurnContext): assert context_arg.activity.conversation.id == "conversation123" assert context_arg.activity.channel_id == "channel_id" - assert context_arg.activity.service_url is None + assert context_arg.activity.service_url == service_url assert ( context_arg.turn_state[ChannelServiceAdapter.USER_TOKEN_CLIENT_KEY] is user_token_client @@ -154,63 +160,6 @@ async def callback(context: TurnContext): not in context_arg.turn_state ) - @pytest.mark.asyncio - @pytest.mark.parametrize( - "delivery_mode", - [DeliveryModes.expect_replies, DeliveryModes.stream], - ) - async def test_process_activity_expect_replies_and_stream_with_service_url( - self, mocker, user_token_client, connector_client, adapter, delivery_mode - ): - user_token_client.get_access_token = mocker.AsyncMock( - return_value="user_token_value" - ) - adapter.run_pipeline = mocker.AsyncMock() - - async def callback(context: TurnContext): - return None - - activity = Activity( # type: ignore - type="message", - conversation={"id": "conversation123"}, - channel_id="channel_id", - delivery_mode=delivery_mode, - service_url="service_url", - ) - - claims_identity = ClaimsIdentity( - { - "aud": "agent_app_id", - "ver": "2.0", - "azp": "outgoing_app_id", - }, - is_authenticated=True, - ) - - await adapter.process_activity( - claims_identity, - activity, - callback, - ) - - adapter.run_pipeline.assert_awaited_once() - - context_arg, callback_arg = adapter.run_pipeline.call_args[0] - assert callback_arg == callback - assert context_arg.activity == activity - - assert context_arg.activity.conversation.id == "conversation123" - assert context_arg.activity.channel_id == "channel_id" - assert context_arg.activity.service_url == "service_url" - assert ( - context_arg.turn_state[ChannelServiceAdapter.USER_TOKEN_CLIENT_KEY] - is user_token_client - ) - assert ( - context_arg.turn_state[ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY] - is connector_client - ) - @pytest.mark.asyncio async def test_process_activity_normal_no_service_url( self, mocker, user_token_client, adapter