async def _send_to_skill( self, context: TurnContext, activity: Activity, skill_conversation_id: str ) -> Activity: if activity.type == ActivityTypes.invoke: # Force ExpectReplies for invoke activities so we can get the replies right away and send # them back to the channel if needed. This makes sure that the dialog will receive the Invoke # response from the skill and any other activities sent, including EoC. activity.delivery_mode = DeliveryModes.expect_replies # Always save state before forwarding # (the dialog stack won't get updated with the skillDialog and things won't work if you don't) await self.dialog_options.conversation_state.save_changes(context, True) skill_info = self.dialog_options.skill response = await self.dialog_options.skill_client.post_activity( self.dialog_options.bot_id, skill_info.app_id, skill_info.skill_endpoint, self.dialog_options.skill_host_endpoint, skill_conversation_id, activity, ) # Inspect the skill response status if not 200 <= response.status <= 299: raise Exception( f'Error invoking the skill id: "{skill_info.id}" at "{skill_info.skill_endpoint}"' f" (status is {response.status}). \r\n {response.body}" ) eoc_activity: Activity = None if activity.delivery_mode == DeliveryModes.expect_replies and response.body: # Process replies in the response.Body. response.body: List[Activity] response.body = ExpectedReplies().deserialize(response.body).activities for from_skill_activity in response.body: if from_skill_activity.type == ActivityTypes.end_of_conversation: # Capture the EndOfConversation activity if it was sent from skill eoc_activity = from_skill_activity # The conversation has ended, so cleanup the conversation id await self.dialog_options.conversation_id_factory.delete_conversation_reference( skill_conversation_id ) elif await self._intercept_oauth_cards( context, from_skill_activity, self.dialog_options.connection_name ): # do nothing. Token exchange succeeded, so no oauthcard needs to be shown to the user pass else: # Send the response back to the channel. await context.send_activity(from_skill_activity) return eoc_activity
async def _send_to_skill( self, context: TurnContext, activity: Activity, ) -> Activity: # Create a conversationId to interact with the skill and send the activity conversation_id_factory_options = SkillConversationIdFactoryOptions( from_bot_oauth_scope=context.turn_state.get( BotAdapter.BOT_OAUTH_SCOPE_KEY), from_bot_id=self.dialog_options.bot_id, activity=activity, bot_framework_skill=self.dialog_options.skill, ) skill_conversation_id = await self.dialog_options.conversation_id_factory.create_skill_conversation_id( conversation_id_factory_options) # Always save state before forwarding # (the dialog stack won't get updated with the skillDialog and things won't work if you don't) skill_info = self.dialog_options.skill await self.dialog_options.conversation_state.save_changes( context, True) response = await self.dialog_options.skill_client.post_activity( self.dialog_options.bot_id, skill_info.app_id, skill_info.skill_endpoint, self.dialog_options.skill_host_endpoint, skill_conversation_id, activity, ) # Inspect the skill response status if not 200 <= response.status <= 299: raise Exception( f'Error invoking the skill id: "{skill_info.id}" at "{skill_info.skill_endpoint}"' f" (status is {response.status}). \r\n {response.body}") eoc_activity: Activity = None if activity.delivery_mode == DeliveryModes.expect_replies and response.body: # Process replies in the response.Body. response.body: List[Activity] response.body = ExpectedReplies().deserialize( response.body).activities for from_skill_activity in response.body: if from_skill_activity.type == ActivityTypes.end_of_conversation: # Capture the EndOfConversation activity if it was sent from skill eoc_activity = from_skill_activity else: # Send the response back to the channel. await context.send_activity(from_skill_activity) return eoc_activity
async def __send_to_skill( self, turn_context: TurnContext, delivery_mode: str, target_skill: BotFrameworkSkill, ): # NOTE: Always SaveChanges() before calling a skill so that any activity generated by the skill # will have access to current accurate state. await self._conversation_state.save_changes(turn_context, force=True) if delivery_mode == "expectReplies": # Clone activity and update its delivery mode. activity = copy.copy(turn_context.activity) activity.delivery_mode = delivery_mode # Route the activity to the skill. expect_replies_response = await self._skill_client.post_activity_to_skill( self._bot_id, target_skill, self._skills_config.SKILL_HOST_ENDPOINT, activity, ) # Route response activities back to the channel. response_activities: ExpectedReplies = ( ExpectedReplies().deserialize(expect_replies_response.body).activities ) for response_activity in response_activities: if response_activity.type == ActivityTypes.end_of_conversation: await self.end_conversation(response_activity, turn_context) # Restart setup dialog. await DialogHelper.run_dialog( self._dialog, turn_context, self._dialog_state_property, ) else: await turn_context.send_activity(response_activity) else: # Route the activity to the skill. await self._skill_client.post_activity_to_skill( self._bot_id, target_skill, self._skills_config.SKILL_HOST_ENDPOINT, turn_context.activity, )
async def test_delivery_mode_expect_replies(self): mock_credential_provider = unittest.mock.create_autospec(CredentialProvider) settings = BotFrameworkAdapterSettings( app_id="bot_id", credential_provider=mock_credential_provider ) adapter = AdapterUnderTest(settings) async def callback(context: TurnContext): await context.send_activity("activity 1") await context.send_activity("activity 2") await context.send_activity("activity 3") inbound_activity = Activity( type=ActivityTypes.message, channel_id="emulator", service_url="http://tempuri.org/whatever", delivery_mode=DeliveryModes.expect_replies, text="hello world", ) identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: "bot_id", AuthenticationConstants.APP_ID_CLAIM: "bot_id", AuthenticationConstants.VERSION_CLAIM: "1.0", }, is_authenticated=True, ) invoke_response = await adapter.process_activity_with_identity( inbound_activity, identity, callback ) assert invoke_response assert invoke_response.status == 200 activities = ExpectedReplies().deserialize(invoke_response.body).activities assert len(activities) == 3 assert activities[0].text == "activity 1" assert activities[1].text == "activity 2" assert activities[2].text == "activity 3" assert ( adapter.connector_client_mock.conversations.send_to_conversation.call_count == 0 )
async def post_buffered_activity( self, from_bot_id: str, to_bot_id: str, to_url: str, service_url: str, conversation_id: str, activity: Activity, ) -> [Activity]: """ Helper method to return a list of activities when an Activity is being sent with DeliveryMode == expectReplies. """ response = await self.post_activity(from_bot_id, to_bot_id, to_url, service_url, conversation_id, activity) if not response or (response.status / 100) != 2: return [] return ExpectedReplies().deserialize(response.body).activities
async def test_should_not_intercept_oauth_cards_for_empty_connection_name( self): connection_name = "connectionName" first_response = ExpectedReplies(activities=[ SkillDialogTests.create_oauth_card_attachment_activity( "https://test") ]) sequence = 0 async def post_return(): nonlocal sequence if sequence == 0: result = InvokeResponse(body=first_response, status=HTTPStatus.OK) else: result = InvokeResponse(status=HTTPStatus.OK) sequence += 1 return result mock_skill_client = self._create_mock_skill_client(None, post_return) conversation_state = ConversationState(MemoryStorage()) dialog_options = SkillDialogTests.create_skill_dialog_options( conversation_state, mock_skill_client) sut = SkillDialog(dialog_options, dialog_id="dialog") activity_to_send = SkillDialogTests.create_send_activity() client = DialogTestClient( "test", sut, BeginSkillDialogOptions(activity=activity_to_send, ), conversation_state=conversation_state, ) client.test_adapter.add_exchangeable_token(connection_name, "test", "User1", "https://test", "https://test1") final_activity = await client.send_activity( MessageFactory.text("irrelevant")) self.assertIsNotNone(final_activity) self.assertEqual(len(final_activity.attachments), 1)
def _process_turn_results(self, context: TurnContext) -> InvokeResponse: # Handle ExpectedReplies scenarios where all activities have been # buffered and sent back at once in an invoke response. if context.activity.delivery_mode == DeliveryModes.expect_replies: return InvokeResponse( status=HTTPStatus.OK, body=ExpectedReplies(activities=context.buffered_reply_activities), ) # Handle Invoke scenarios where the bot will return a specific body and return code. if context.activity.type == ActivityTypes.invoke: activity_invoke_response: Activity = context.turn_state.get( self._INVOKE_RESPONSE_KEY ) if not activity_invoke_response: return InvokeResponse(status=HTTPStatus.NOT_IMPLEMENTED) return activity_invoke_response.value # No body to return return None
def _create_mock_skill_client( self, callback: Callable, return_status: Union[Callable, int] = 200, expected_replies: List[Activity] = None, ) -> BotFrameworkClient: mock_client = Mock() activity_list = ExpectedReplies( activities=expected_replies or [MessageFactory.text("dummy activity")] ) async def mock_post_activity( from_bot_id: str, to_bot_id: str, to_url: str, service_url: str, conversation_id: str, activity: Activity, ): nonlocal callback, return_status if callback: await callback( from_bot_id, to_bot_id, to_url, service_url, conversation_id, activity, ) if isinstance(return_status, Callable): return await return_status() return InvokeResponse(status=return_status, body=activity_list) mock_client.post_activity.side_effect = mock_post_activity return mock_client