diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/typing_indicator.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/typing_indicator.py index a99ddc30..9c9a967f 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/typing_indicator.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/typing_indicator.py @@ -124,7 +124,7 @@ def __init__( self._stopped: bool = False self._hook_registered: bool = False - async def __aenter__(self) -> "TypingIndicator": + async def __aenter__(self) -> TypingIndicator: self.start() return self 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 6cbc700f..cd57a5c8 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 @@ -69,12 +69,19 @@ async def send_activities( raise ValueError("send_activities: activities list cannot be empty") responses = [] + buffered_reply_activities = [] for activity in activities: activity.id = None response = ResourceResponse() + buffer_replies = ( + context.activity.delivery_mode == DeliveryModes.expect_replies + ) + if buffer_replies: + buffered_reply_activities.append(activity) + if activity.type == ActivityTypes.invoke_response: context.turn_state[self.INVOKE_RESPONSE_KEY] = activity elif ( @@ -83,7 +90,7 @@ async def send_activities( ): # no-op pass - else: + elif not buffer_replies: connector_client = context.services.get(ConnectorClientBase) if not connector_client: raise RuntimeError( @@ -110,6 +117,8 @@ async def send_activities( responses.append(response) + if buffered_reply_activities: + context.buffered_reply_activities.extend(buffered_reply_activities) return responses async def update_activity(self, context: TurnContext, activity: Activity): diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py index f0d596aa..ddbeaea5 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py @@ -12,7 +12,6 @@ Activity, ActivityTypes, ConversationReference, - DeliveryModes, InputHints, Mention, ResourceResponse, @@ -47,9 +46,6 @@ def __call__(self) -> Awaitable[T]: ... class TurnContext(TurnContextProtocol): - # Same constant as in the BF Adapter, duplicating here to avoid circular dependency - _INVOKE_RESPONSE_KEY = "TurnContext.InvokeResponse" - _activity: Activity _on_send_activities: list[OnSendActivitiesHandler] @@ -203,7 +199,6 @@ async def send_activities( self, activities: list[Activity] ) -> list[ResourceResponse]: sent_non_trace_activity = False - # TODO: Check activity serialization ref = self.activity.get_conversation_reference() with spans.TurnContextSendActivities(self): @@ -229,23 +224,6 @@ def activity_validator(activity: Activity) -> Activity: # send activities through adapter async def logic() -> list[ResourceResponse]: nonlocal sent_non_trace_activity - - if self.activity.delivery_mode == DeliveryModes.expect_replies: - responses = [] - for activity in output: - self.buffered_reply_activities.append(activity) - # Ensure the TurnState has the InvokeResponseKey, since this activity - # is not being sent through the adapter, where it would be added to TurnState. - if activity.type == ActivityTypes.invoke_response: - self.turn_state[TurnContext._INVOKE_RESPONSE_KEY] = activity - - responses.append(ResourceResponse()) - - if sent_non_trace_activity: - self.responded = True - - return responses - responses = await self.adapter.send_activities(self, output) if sent_non_trace_activity: self.responded = True diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py index 805a0782..3530c77a 100644 --- a/tests/hosting_core/test_channel_service_adapter.py +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -2,6 +2,7 @@ from microsoft_agents.activity import ( Activity, + ActivityTypes, ConversationResourceResponse, ConversationParameters, DeliveryModes, @@ -160,6 +161,30 @@ async def callback(context: TurnContext): assert context_arg.services.get(UserTokenClientBase) is user_token_client assert not context_arg.services.has(ConnectorClientBase) + @pytest.mark.asyncio + async def test_send_activities_buffers_expect_replies_without_connector( + self, adapter + ): + context = TurnContext( + adapter, + Activity( + type=ActivityTypes.message, + conversation={"id": "conversation123"}, + channel_id="channel_id", + delivery_mode=DeliveryModes.expect_replies, + ), + ) + activities = [ + Activity(type=ActivityTypes.message, text="reply"), + Activity(type=ActivityTypes.typing), + ] + + responses = await adapter.send_activities(context, activities) + + assert len(responses) == 2 + assert context.buffered_reply_activities == activities + assert not context.services.has(ConnectorClientBase) + @pytest.mark.asyncio async def test_process_activity_normal_no_service_url( self, mocker, user_token_client, adapter