We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
SkillDialog doesn't call SkillConversationIdFactory.DeleteConversationReference when using ExpectReplies
See [parent](https://github.com/microsoft/botframework-sdk/issues/6019)

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py`
Content:
```
1 # Copyright (c) Microsoft Corporation. All rights reserved.
2 # Licensed under the MIT License.
3 
4 from copy import deepcopy
5 from typing import List
6 
7 from botframework.connector.token_api.models import TokenExchangeRequest
8 from botbuilder.schema import (
9     Activity,
10     ActivityTypes,
11     ExpectedReplies,
12     DeliveryModes,
13     SignInConstants,
14     TokenExchangeInvokeRequest,
15 )
16 from botbuilder.core import BotAdapter, TurnContext, ExtendedUserTokenProvider
17 from botbuilder.core.card_factory import ContentTypes
18 from botbuilder.core.skills import SkillConversationIdFactoryOptions
19 from botbuilder.dialogs import (
20     Dialog,
21     DialogContext,
22     DialogEvents,
23     DialogReason,
24     DialogInstance,
25 )
26 
27 from .begin_skill_dialog_options import BeginSkillDialogOptions
28 from .skill_dialog_options import SkillDialogOptions
29 
30 
31 class SkillDialog(Dialog):
32     SKILLCONVERSATIONIDSTATEKEY = (
33         "Microsoft.Bot.Builder.Dialogs.SkillDialog.SkillConversationId"
34     )
35 
36     def __init__(self, dialog_options: SkillDialogOptions, dialog_id: str):
37         super().__init__(dialog_id)
38         if not dialog_options:
39             raise TypeError("SkillDialog.__init__(): dialog_options cannot be None.")
40 
41         self.dialog_options = dialog_options
42         self._deliver_mode_state_key = "deliverymode"
43 
44     async def begin_dialog(self, dialog_context: DialogContext, options: object = None):
45         """
46         Method called when a new dialog has been pushed onto the stack and is being activated.
47         :param dialog_context: The dialog context for the current turn of conversation.
48         :param options: (Optional) additional argument(s) to pass to the dialog being started.
49         """
50         dialog_args = self._validate_begin_dialog_args(options)
51 
52         await dialog_context.context.send_trace_activity(
53             f"{SkillDialog.__name__}.BeginDialogAsync()",
54             label=f"Using activity of type: {dialog_args.activity.type}",
55         )
56 
57         # Create deep clone of the original activity to avoid altering it before forwarding it.
58         skill_activity: Activity = deepcopy(dialog_args.activity)
59 
60         # Apply conversation reference and common properties from incoming activity before sending.
61         TurnContext.apply_conversation_reference(
62             skill_activity,
63             TurnContext.get_conversation_reference(dialog_context.context.activity),
64             is_incoming=True,
65         )
66 
67         # Store delivery mode in dialog state for later use.
68         dialog_context.active_dialog.state[
69             self._deliver_mode_state_key
70         ] = dialog_args.activity.delivery_mode
71 
72         # Create the conversationId and store it in the dialog context state so we can use it later
73         skill_conversation_id = await self._create_skill_conversation_id(
74             dialog_context.context, dialog_context.context.activity
75         )
76         dialog_context.active_dialog.state[
77             SkillDialog.SKILLCONVERSATIONIDSTATEKEY
78         ] = skill_conversation_id
79 
80         # Send the activity to the skill.
81         eoc_activity = await self._send_to_skill(
82             dialog_context.context, skill_activity, skill_conversation_id
83         )
84         if eoc_activity:
85             return await dialog_context.end_dialog(eoc_activity.value)
86 
87         return self.end_of_turn
88 
89     async def continue_dialog(self, dialog_context: DialogContext):
90         if not self._on_validate_activity(dialog_context.context.activity):
91             return self.end_of_turn
92 
93         await dialog_context.context.send_trace_activity(
94             f"{SkillDialog.__name__}.continue_dialog()",
95             label=f"ActivityType: {dialog_context.context.activity.type}",
96         )
97 
98         # Handle EndOfConversation from the skill (this will be sent to the this dialog by the SkillHandler if
99         # received from the Skill)
100         if dialog_context.context.activity.type == ActivityTypes.end_of_conversation:
101             await dialog_context.context.send_trace_activity(
102                 f"{SkillDialog.__name__}.continue_dialog()",
103                 label=f"Got {ActivityTypes.end_of_conversation}",
104             )
105 
106             return await dialog_context.end_dialog(
107                 dialog_context.context.activity.value
108             )
109 
110         # Create deep clone of the original activity to avoid altering it before forwarding it.
111         skill_activity = deepcopy(dialog_context.context.activity)
112 
113         skill_activity.delivery_mode = dialog_context.active_dialog.state[
114             self._deliver_mode_state_key
115         ]
116 
117         # Just forward to the remote skill
118         skill_conversation_id = dialog_context.active_dialog.state[
119             SkillDialog.SKILLCONVERSATIONIDSTATEKEY
120         ]
121         eoc_activity = await self._send_to_skill(
122             dialog_context.context, skill_activity, skill_conversation_id
123         )
124         if eoc_activity:
125             return await dialog_context.end_dialog(eoc_activity.value)
126 
127         return self.end_of_turn
128 
129     async def reprompt_dialog(  # pylint: disable=unused-argument
130         self, context: TurnContext, instance: DialogInstance
131     ):
132         # Create and send an event to the skill so it can resume the dialog.
133         reprompt_event = Activity(
134             type=ActivityTypes.event, name=DialogEvents.reprompt_dialog
135         )
136 
137         # Apply conversation reference and common properties from incoming activity before sending.
138         TurnContext.apply_conversation_reference(
139             reprompt_event,
140             TurnContext.get_conversation_reference(context.activity),
141             is_incoming=True,
142         )
143 
144         # connection Name is not applicable for a RePrompt, as we don't expect as OAuthCard in response.
145         skill_conversation_id = instance.state[SkillDialog.SKILLCONVERSATIONIDSTATEKEY]
146         await self._send_to_skill(context, reprompt_event, skill_conversation_id)
147 
148     async def resume_dialog(  # pylint: disable=unused-argument
149         self, dialog_context: "DialogContext", reason: DialogReason, result: object
150     ):
151         await self.reprompt_dialog(dialog_context.context, dialog_context.active_dialog)
152         return self.end_of_turn
153 
154     async def end_dialog(
155         self, context: TurnContext, instance: DialogInstance, reason: DialogReason
156     ):
157         # Send of of conversation to the skill if the dialog has been cancelled.
158         if reason in (DialogReason.CancelCalled, DialogReason.ReplaceCalled):
159             await context.send_trace_activity(
160                 f"{SkillDialog.__name__}.end_dialog()",
161                 label=f"ActivityType: {context.activity.type}",
162             )
163             activity = Activity(type=ActivityTypes.end_of_conversation)
164 
165             # Apply conversation reference and common properties from incoming activity before sending.
166             TurnContext.apply_conversation_reference(
167                 activity,
168                 TurnContext.get_conversation_reference(context.activity),
169                 is_incoming=True,
170             )
171             activity.channel_data = context.activity.channel_data
172             activity.additional_properties = context.activity.additional_properties
173 
174             # connection Name is not applicable for an EndDialog, as we don't expect as OAuthCard in response.
175             skill_conversation_id = instance.state[
176                 SkillDialog.SKILLCONVERSATIONIDSTATEKEY
177             ]
178             await self._send_to_skill(context, activity, skill_conversation_id)
179 
180         await super().end_dialog(context, instance, reason)
181 
182     def _validate_begin_dialog_args(self, options: object) -> BeginSkillDialogOptions:
183         if not options:
184             raise TypeError("options cannot be None.")
185 
186         dialog_args = BeginSkillDialogOptions.from_object(options)
187 
188         if not dialog_args:
189             raise TypeError(
190                 "SkillDialog: options object not valid as BeginSkillDialogOptions."
191             )
192 
193         if not dialog_args.activity:
194             raise TypeError(
195                 "SkillDialog: activity object in options as BeginSkillDialogOptions cannot be None."
196             )
197 
198         return dialog_args
199 
200     def _on_validate_activity(
201         self, activity: Activity  # pylint: disable=unused-argument
202     ) -> bool:
203         """
204         Validates the activity sent during continue_dialog.
205 
206         Override this method to implement a custom validator for the activity being sent during continue_dialog.
207         This method can be used to ignore activities of a certain type if needed.
208         If this method returns false, the dialog will end the turn without processing the activity.
209         """
210         return True
211 
212     async def _send_to_skill(
213         self, context: TurnContext, activity: Activity, skill_conversation_id: str
214     ) -> Activity:
215         if activity.type == ActivityTypes.invoke:
216             # Force ExpectReplies for invoke activities so we can get the replies right away and send
217             # them back to the channel if needed. This makes sure that the dialog will receive the Invoke
218             # response from the skill and any other activities sent, including EoC.
219             activity.delivery_mode = DeliveryModes.expect_replies
220 
221         # Always save state before forwarding
222         # (the dialog stack won't get updated with the skillDialog and things won't work if you don't)
223         await self.dialog_options.conversation_state.save_changes(context, True)
224 
225         skill_info = self.dialog_options.skill
226         response = await self.dialog_options.skill_client.post_activity(
227             self.dialog_options.bot_id,
228             skill_info.app_id,
229             skill_info.skill_endpoint,
230             self.dialog_options.skill_host_endpoint,
231             skill_conversation_id,
232             activity,
233         )
234 
235         # Inspect the skill response status
236         if not 200 <= response.status <= 299:
237             raise Exception(
238                 f'Error invoking the skill id: "{skill_info.id}" at "{skill_info.skill_endpoint}"'
239                 f" (status is {response.status}). \r\n {response.body}"
240             )
241 
242         eoc_activity: Activity = None
243         if activity.delivery_mode == DeliveryModes.expect_replies and response.body:
244             # Process replies in the response.Body.
245             response.body: List[Activity]
246             response.body = ExpectedReplies().deserialize(response.body).activities
247 
248             for from_skill_activity in response.body:
249                 if from_skill_activity.type == ActivityTypes.end_of_conversation:
250                     # Capture the EndOfConversation activity if it was sent from skill
251                     eoc_activity = from_skill_activity
252                 elif await self._intercept_oauth_cards(
253                     context, from_skill_activity, self.dialog_options.connection_name
254                 ):
255                     # do nothing. Token exchange succeeded, so no oauthcard needs to be shown to the user
256                     pass
257                 else:
258                     # Send the response back to the channel.
259                     await context.send_activity(from_skill_activity)
260 
261         return eoc_activity
262 
263     async def _create_skill_conversation_id(
264         self, context: TurnContext, activity: Activity
265     ) -> str:
266         # Create a conversationId to interact with the skill and send the activity
267         conversation_id_factory_options = SkillConversationIdFactoryOptions(
268             from_bot_oauth_scope=context.turn_state.get(BotAdapter.BOT_OAUTH_SCOPE_KEY),
269             from_bot_id=self.dialog_options.bot_id,
270             activity=activity,
271             bot_framework_skill=self.dialog_options.skill,
272         )
273         skill_conversation_id = await self.dialog_options.conversation_id_factory.create_skill_conversation_id(
274             conversation_id_factory_options
275         )
276         return skill_conversation_id
277 
278     async def _intercept_oauth_cards(
279         self, context: TurnContext, activity: Activity, connection_name: str
280     ):
281         """
282         Tells is if we should intercept the OAuthCard message.
283         """
284         if not connection_name or not isinstance(
285             context.adapter, ExtendedUserTokenProvider
286         ):
287             # The adapter may choose not to support token exchange, in which case we fallback to
288             # showing an oauth card to the user.
289             return False
290 
291         oauth_card_attachment = next(
292             attachment
293             for attachment in activity.attachments
294             if attachment.content_type == ContentTypes.oauth_card
295         )
296         if oauth_card_attachment:
297             oauth_card = oauth_card_attachment.content
298             if (
299                 oauth_card
300                 and oauth_card.token_exchange_resource
301                 and oauth_card.token_exchange_resource.uri
302             ):
303                 try:
304                     result = await context.adapter.exchange_token(
305                         turn_context=context,
306                         connection_name=connection_name,
307                         user_id=context.activity.from_property.id,
308                         exchange_request=TokenExchangeRequest(
309                             uri=oauth_card.token_exchange_resource.uri
310                         ),
311                     )
312 
313                     if result and result.token:
314                         # If token above is null, then SSO has failed and hence we return false.
315                         # If not, send an invoke to the skill with the token.
316                         return await self._send_token_exchange_invoke_to_skill(
317                             activity,
318                             oauth_card.token_exchange_resource.id,
319                             oauth_card.connection_name,
320                             result.token,
321                         )
322                 except:
323                     # Failures in token exchange are not fatal. They simply mean that the user needs
324                     # to be shown the OAuth card.
325                     return False
326 
327         return False
328 
329     async def _send_token_exchange_invoke_to_skill(
330         self,
331         incoming_activity: Activity,
332         request_id: str,
333         connection_name: str,
334         token: str,
335     ):
336         activity = incoming_activity.create_reply()
337         activity.type = ActivityTypes.invoke
338         activity.name = SignInConstants.token_exchange_operation_name
339         activity.value = TokenExchangeInvokeRequest(
340             id=request_id, token=token, connection_name=connection_name,
341         )
342 
343         # route the activity to the skill
344         skill_info = self.dialog_options.skill
345         response = await self.dialog_options.skill_client.post_activity(
346             self.dialog_options.bot_id,
347             skill_info.app_id,
348             skill_info.skill_endpoint,
349             self.dialog_options.skill_host_endpoint,
350             incoming_activity.conversation.id,
351             activity,
352         )
353 
354         # Check response status: true if success, false if failure
355         return response.is_successful_status_code()
356
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py
@@ -249,6 +249,11 @@
                 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
                 ):

{"golden_diff": "diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py\n--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py\n+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py\n@@ -249,6 +249,11 @@\n                 if from_skill_activity.type == ActivityTypes.end_of_conversation:\n                     # Capture the EndOfConversation activity if it was sent from skill\n                     eoc_activity = from_skill_activity\n+\n+                    # The conversation has ended, so cleanup the conversation id\n+                    await self.dialog_options.conversation_id_factory.delete_conversation_reference(\n+                        skill_conversation_id\n+                    )\n                 elif await self._intercept_oauth_cards(\n                     context, from_skill_activity, self.dialog_options.connection_name\n                 ):\n", "issue": "SkillDialog doesn't call SkillConversationIdFactory.DeleteConversationReference when using ExpectReplies\nSee [parent](https://github.com/microsoft/botframework-sdk/issues/6019)\n", "before_files": [{"content": "# Copyright (c) Microsoft Corporation. All rights reserved.\n# Licensed under the MIT License.\n\nfrom copy import deepcopy\nfrom typing import List\n\nfrom botframework.connector.token_api.models import TokenExchangeRequest\nfrom botbuilder.schema import (\n    Activity,\n    ActivityTypes,\n    ExpectedReplies,\n    DeliveryModes,\n    SignInConstants,\n    TokenExchangeInvokeRequest,\n)\nfrom botbuilder.core import BotAdapter, TurnContext, ExtendedUserTokenProvider\nfrom botbuilder.core.card_factory import ContentTypes\nfrom botbuilder.core.skills import SkillConversationIdFactoryOptions\nfrom botbuilder.dialogs import (\n    Dialog,\n    DialogContext,\n    DialogEvents,\n    DialogReason,\n    DialogInstance,\n)\n\nfrom .begin_skill_dialog_options import BeginSkillDialogOptions\nfrom .skill_dialog_options import SkillDialogOptions\n\n\nclass SkillDialog(Dialog):\n    SKILLCONVERSATIONIDSTATEKEY = (\n        \"Microsoft.Bot.Builder.Dialogs.SkillDialog.SkillConversationId\"\n    )\n\n    def __init__(self, dialog_options: SkillDialogOptions, dialog_id: str):\n        super().__init__(dialog_id)\n        if not dialog_options:\n            raise TypeError(\"SkillDialog.__init__(): dialog_options cannot be None.\")\n\n        self.dialog_options = dialog_options\n        self._deliver_mode_state_key = \"deliverymode\"\n\n    async def begin_dialog(self, dialog_context: DialogContext, options: object = None):\n        \"\"\"\n        Method called when a new dialog has been pushed onto the stack and is being activated.\n        :param dialog_context: The dialog context for the current turn of conversation.\n        :param options: (Optional) additional argument(s) to pass to the dialog being started.\n        \"\"\"\n        dialog_args = self._validate_begin_dialog_args(options)\n\n        await dialog_context.context.send_trace_activity(\n            f\"{SkillDialog.__name__}.BeginDialogAsync()\",\n            label=f\"Using activity of type: {dialog_args.activity.type}\",\n        )\n\n        # Create deep clone of the original activity to avoid altering it before forwarding it.\n        skill_activity: Activity = deepcopy(dialog_args.activity)\n\n        # Apply conversation reference and common properties from incoming activity before sending.\n        TurnContext.apply_conversation_reference(\n            skill_activity,\n            TurnContext.get_conversation_reference(dialog_context.context.activity),\n            is_incoming=True,\n        )\n\n        # Store delivery mode in dialog state for later use.\n        dialog_context.active_dialog.state[\n            self._deliver_mode_state_key\n        ] = dialog_args.activity.delivery_mode\n\n        # Create the conversationId and store it in the dialog context state so we can use it later\n        skill_conversation_id = await self._create_skill_conversation_id(\n            dialog_context.context, dialog_context.context.activity\n        )\n        dialog_context.active_dialog.state[\n            SkillDialog.SKILLCONVERSATIONIDSTATEKEY\n        ] = skill_conversation_id\n\n        # Send the activity to the skill.\n        eoc_activity = await self._send_to_skill(\n            dialog_context.context, skill_activity, skill_conversation_id\n        )\n        if eoc_activity:\n            return await dialog_context.end_dialog(eoc_activity.value)\n\n        return self.end_of_turn\n\n    async def continue_dialog(self, dialog_context: DialogContext):\n        if not self._on_validate_activity(dialog_context.context.activity):\n            return self.end_of_turn\n\n        await dialog_context.context.send_trace_activity(\n            f\"{SkillDialog.__name__}.continue_dialog()\",\n            label=f\"ActivityType: {dialog_context.context.activity.type}\",\n        )\n\n        # Handle EndOfConversation from the skill (this will be sent to the this dialog by the SkillHandler if\n        # received from the Skill)\n        if dialog_context.context.activity.type == ActivityTypes.end_of_conversation:\n            await dialog_context.context.send_trace_activity(\n                f\"{SkillDialog.__name__}.continue_dialog()\",\n                label=f\"Got {ActivityTypes.end_of_conversation}\",\n            )\n\n            return await dialog_context.end_dialog(\n                dialog_context.context.activity.value\n            )\n\n        # Create deep clone of the original activity to avoid altering it before forwarding it.\n        skill_activity = deepcopy(dialog_context.context.activity)\n\n        skill_activity.delivery_mode = dialog_context.active_dialog.state[\n            self._deliver_mode_state_key\n        ]\n\n        # Just forward to the remote skill\n        skill_conversation_id = dialog_context.active_dialog.state[\n            SkillDialog.SKILLCONVERSATIONIDSTATEKEY\n        ]\n        eoc_activity = await self._send_to_skill(\n            dialog_context.context, skill_activity, skill_conversation_id\n        )\n        if eoc_activity:\n            return await dialog_context.end_dialog(eoc_activity.value)\n\n        return self.end_of_turn\n\n    async def reprompt_dialog(  # pylint: disable=unused-argument\n        self, context: TurnContext, instance: DialogInstance\n    ):\n        # Create and send an event to the skill so it can resume the dialog.\n        reprompt_event = Activity(\n            type=ActivityTypes.event, name=DialogEvents.reprompt_dialog\n        )\n\n        # Apply conversation reference and common properties from incoming activity before sending.\n        TurnContext.apply_conversation_reference(\n            reprompt_event,\n            TurnContext.get_conversation_reference(context.activity),\n            is_incoming=True,\n        )\n\n        # connection Name is not applicable for a RePrompt, as we don't expect as OAuthCard in response.\n        skill_conversation_id = instance.state[SkillDialog.SKILLCONVERSATIONIDSTATEKEY]\n        await self._send_to_skill(context, reprompt_event, skill_conversation_id)\n\n    async def resume_dialog(  # pylint: disable=unused-argument\n        self, dialog_context: \"DialogContext\", reason: DialogReason, result: object\n    ):\n        await self.reprompt_dialog(dialog_context.context, dialog_context.active_dialog)\n        return self.end_of_turn\n\n    async def end_dialog(\n        self, context: TurnContext, instance: DialogInstance, reason: DialogReason\n    ):\n        # Send of of conversation to the skill if the dialog has been cancelled.\n        if reason in (DialogReason.CancelCalled, DialogReason.ReplaceCalled):\n            await context.send_trace_activity(\n                f\"{SkillDialog.__name__}.end_dialog()\",\n                label=f\"ActivityType: {context.activity.type}\",\n            )\n            activity = Activity(type=ActivityTypes.end_of_conversation)\n\n            # Apply conversation reference and common properties from incoming activity before sending.\n            TurnContext.apply_conversation_reference(\n                activity,\n                TurnContext.get_conversation_reference(context.activity),\n                is_incoming=True,\n            )\n            activity.channel_data = context.activity.channel_data\n            activity.additional_properties = context.activity.additional_properties\n\n            # connection Name is not applicable for an EndDialog, as we don't expect as OAuthCard in response.\n            skill_conversation_id = instance.state[\n                SkillDialog.SKILLCONVERSATIONIDSTATEKEY\n            ]\n            await self._send_to_skill(context, activity, skill_conversation_id)\n\n        await super().end_dialog(context, instance, reason)\n\n    def _validate_begin_dialog_args(self, options: object) -> BeginSkillDialogOptions:\n        if not options:\n            raise TypeError(\"options cannot be None.\")\n\n        dialog_args = BeginSkillDialogOptions.from_object(options)\n\n        if not dialog_args:\n            raise TypeError(\n                \"SkillDialog: options object not valid as BeginSkillDialogOptions.\"\n            )\n\n        if not dialog_args.activity:\n            raise TypeError(\n                \"SkillDialog: activity object in options as BeginSkillDialogOptions cannot be None.\"\n            )\n\n        return dialog_args\n\n    def _on_validate_activity(\n        self, activity: Activity  # pylint: disable=unused-argument\n    ) -> bool:\n        \"\"\"\n        Validates the activity sent during continue_dialog.\n\n        Override this method to implement a custom validator for the activity being sent during continue_dialog.\n        This method can be used to ignore activities of a certain type if needed.\n        If this method returns false, the dialog will end the turn without processing the activity.\n        \"\"\"\n        return True\n\n    async def _send_to_skill(\n        self, context: TurnContext, activity: Activity, skill_conversation_id: str\n    ) -> Activity:\n        if activity.type == ActivityTypes.invoke:\n            # Force ExpectReplies for invoke activities so we can get the replies right away and send\n            # them back to the channel if needed. This makes sure that the dialog will receive the Invoke\n            # response from the skill and any other activities sent, including EoC.\n            activity.delivery_mode = DeliveryModes.expect_replies\n\n        # Always save state before forwarding\n        # (the dialog stack won't get updated with the skillDialog and things won't work if you don't)\n        await self.dialog_options.conversation_state.save_changes(context, True)\n\n        skill_info = self.dialog_options.skill\n        response = await self.dialog_options.skill_client.post_activity(\n            self.dialog_options.bot_id,\n            skill_info.app_id,\n            skill_info.skill_endpoint,\n            self.dialog_options.skill_host_endpoint,\n            skill_conversation_id,\n            activity,\n        )\n\n        # Inspect the skill response status\n        if not 200 <= response.status <= 299:\n            raise Exception(\n                f'Error invoking the skill id: \"{skill_info.id}\" at \"{skill_info.skill_endpoint}\"'\n                f\" (status is {response.status}). \\r\\n {response.body}\"\n            )\n\n        eoc_activity: Activity = None\n        if activity.delivery_mode == DeliveryModes.expect_replies and response.body:\n            # Process replies in the response.Body.\n            response.body: List[Activity]\n            response.body = ExpectedReplies().deserialize(response.body).activities\n\n            for from_skill_activity in response.body:\n                if from_skill_activity.type == ActivityTypes.end_of_conversation:\n                    # Capture the EndOfConversation activity if it was sent from skill\n                    eoc_activity = from_skill_activity\n                elif await self._intercept_oauth_cards(\n                    context, from_skill_activity, self.dialog_options.connection_name\n                ):\n                    # do nothing. Token exchange succeeded, so no oauthcard needs to be shown to the user\n                    pass\n                else:\n                    # Send the response back to the channel.\n                    await context.send_activity(from_skill_activity)\n\n        return eoc_activity\n\n    async def _create_skill_conversation_id(\n        self, context: TurnContext, activity: Activity\n    ) -> str:\n        # Create a conversationId to interact with the skill and send the activity\n        conversation_id_factory_options = SkillConversationIdFactoryOptions(\n            from_bot_oauth_scope=context.turn_state.get(BotAdapter.BOT_OAUTH_SCOPE_KEY),\n            from_bot_id=self.dialog_options.bot_id,\n            activity=activity,\n            bot_framework_skill=self.dialog_options.skill,\n        )\n        skill_conversation_id = await self.dialog_options.conversation_id_factory.create_skill_conversation_id(\n            conversation_id_factory_options\n        )\n        return skill_conversation_id\n\n    async def _intercept_oauth_cards(\n        self, context: TurnContext, activity: Activity, connection_name: str\n    ):\n        \"\"\"\n        Tells is if we should intercept the OAuthCard message.\n        \"\"\"\n        if not connection_name or not isinstance(\n            context.adapter, ExtendedUserTokenProvider\n        ):\n            # The adapter may choose not to support token exchange, in which case we fallback to\n            # showing an oauth card to the user.\n            return False\n\n        oauth_card_attachment = next(\n            attachment\n            for attachment in activity.attachments\n            if attachment.content_type == ContentTypes.oauth_card\n        )\n        if oauth_card_attachment:\n            oauth_card = oauth_card_attachment.content\n            if (\n                oauth_card\n                and oauth_card.token_exchange_resource\n                and oauth_card.token_exchange_resource.uri\n            ):\n                try:\n                    result = await context.adapter.exchange_token(\n                        turn_context=context,\n                        connection_name=connection_name,\n                        user_id=context.activity.from_property.id,\n                        exchange_request=TokenExchangeRequest(\n                            uri=oauth_card.token_exchange_resource.uri\n                        ),\n                    )\n\n                    if result and result.token:\n                        # If token above is null, then SSO has failed and hence we return false.\n                        # If not, send an invoke to the skill with the token.\n                        return await self._send_token_exchange_invoke_to_skill(\n                            activity,\n                            oauth_card.token_exchange_resource.id,\n                            oauth_card.connection_name,\n                            result.token,\n                        )\n                except:\n                    # Failures in token exchange are not fatal. They simply mean that the user needs\n                    # to be shown the OAuth card.\n                    return False\n\n        return False\n\n    async def _send_token_exchange_invoke_to_skill(\n        self,\n        incoming_activity: Activity,\n        request_id: str,\n        connection_name: str,\n        token: str,\n    ):\n        activity = incoming_activity.create_reply()\n        activity.type = ActivityTypes.invoke\n        activity.name = SignInConstants.token_exchange_operation_name\n        activity.value = TokenExchangeInvokeRequest(\n            id=request_id, token=token, connection_name=connection_name,\n        )\n\n        # route the activity to the skill\n        skill_info = self.dialog_options.skill\n        response = await self.dialog_options.skill_client.post_activity(\n            self.dialog_options.bot_id,\n            skill_info.app_id,\n            skill_info.skill_endpoint,\n            self.dialog_options.skill_host_endpoint,\n            incoming_activity.conversation.id,\n            activity,\n        )\n\n        # Check response status: true if success, false if failure\n        return response.is_successful_status_code()\n", "path": "libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py"}], "after_files": [{"content": "# Copyright (c) Microsoft Corporation. All rights reserved.\n# Licensed under the MIT License.\n\nfrom copy import deepcopy\nfrom typing import List\n\nfrom botframework.connector.token_api.models import TokenExchangeRequest\nfrom botbuilder.schema import (\n    Activity,\n    ActivityTypes,\n    ExpectedReplies,\n    DeliveryModes,\n    SignInConstants,\n    TokenExchangeInvokeRequest,\n)\nfrom botbuilder.core import BotAdapter, TurnContext, ExtendedUserTokenProvider\nfrom botbuilder.core.card_factory import ContentTypes\nfrom botbuilder.core.skills import SkillConversationIdFactoryOptions\nfrom botbuilder.dialogs import (\n    Dialog,\n    DialogContext,\n    DialogEvents,\n    DialogReason,\n    DialogInstance,\n)\n\nfrom .begin_skill_dialog_options import BeginSkillDialogOptions\nfrom .skill_dialog_options import SkillDialogOptions\n\n\nclass SkillDialog(Dialog):\n    SKILLCONVERSATIONIDSTATEKEY = (\n        \"Microsoft.Bot.Builder.Dialogs.SkillDialog.SkillConversationId\"\n    )\n\n    def __init__(self, dialog_options: SkillDialogOptions, dialog_id: str):\n        super().__init__(dialog_id)\n        if not dialog_options:\n            raise TypeError(\"SkillDialog.__init__(): dialog_options cannot be None.\")\n\n        self.dialog_options = dialog_options\n        self._deliver_mode_state_key = \"deliverymode\"\n\n    async def begin_dialog(self, dialog_context: DialogContext, options: object = None):\n        \"\"\"\n        Method called when a new dialog has been pushed onto the stack and is being activated.\n        :param dialog_context: The dialog context for the current turn of conversation.\n        :param options: (Optional) additional argument(s) to pass to the dialog being started.\n        \"\"\"\n        dialog_args = self._validate_begin_dialog_args(options)\n\n        await dialog_context.context.send_trace_activity(\n            f\"{SkillDialog.__name__}.BeginDialogAsync()\",\n            label=f\"Using activity of type: {dialog_args.activity.type}\",\n        )\n\n        # Create deep clone of the original activity to avoid altering it before forwarding it.\n        skill_activity: Activity = deepcopy(dialog_args.activity)\n\n        # Apply conversation reference and common properties from incoming activity before sending.\n        TurnContext.apply_conversation_reference(\n            skill_activity,\n            TurnContext.get_conversation_reference(dialog_context.context.activity),\n            is_incoming=True,\n        )\n\n        # Store delivery mode in dialog state for later use.\n        dialog_context.active_dialog.state[\n            self._deliver_mode_state_key\n        ] = dialog_args.activity.delivery_mode\n\n        # Create the conversationId and store it in the dialog context state so we can use it later\n        skill_conversation_id = await self._create_skill_conversation_id(\n            dialog_context.context, dialog_context.context.activity\n        )\n        dialog_context.active_dialog.state[\n            SkillDialog.SKILLCONVERSATIONIDSTATEKEY\n        ] = skill_conversation_id\n\n        # Send the activity to the skill.\n        eoc_activity = await self._send_to_skill(\n            dialog_context.context, skill_activity, skill_conversation_id\n        )\n        if eoc_activity:\n            return await dialog_context.end_dialog(eoc_activity.value)\n\n        return self.end_of_turn\n\n    async def continue_dialog(self, dialog_context: DialogContext):\n        if not self._on_validate_activity(dialog_context.context.activity):\n            return self.end_of_turn\n\n        await dialog_context.context.send_trace_activity(\n            f\"{SkillDialog.__name__}.continue_dialog()\",\n            label=f\"ActivityType: {dialog_context.context.activity.type}\",\n        )\n\n        # Handle EndOfConversation from the skill (this will be sent to the this dialog by the SkillHandler if\n        # received from the Skill)\n        if dialog_context.context.activity.type == ActivityTypes.end_of_conversation:\n            await dialog_context.context.send_trace_activity(\n                f\"{SkillDialog.__name__}.continue_dialog()\",\n                label=f\"Got {ActivityTypes.end_of_conversation}\",\n            )\n\n            return await dialog_context.end_dialog(\n                dialog_context.context.activity.value\n            )\n\n        # Create deep clone of the original activity to avoid altering it before forwarding it.\n        skill_activity = deepcopy(dialog_context.context.activity)\n\n        skill_activity.delivery_mode = dialog_context.active_dialog.state[\n            self._deliver_mode_state_key\n        ]\n\n        # Just forward to the remote skill\n        skill_conversation_id = dialog_context.active_dialog.state[\n            SkillDialog.SKILLCONVERSATIONIDSTATEKEY\n        ]\n        eoc_activity = await self._send_to_skill(\n            dialog_context.context, skill_activity, skill_conversation_id\n        )\n        if eoc_activity:\n            return await dialog_context.end_dialog(eoc_activity.value)\n\n        return self.end_of_turn\n\n    async def reprompt_dialog(  # pylint: disable=unused-argument\n        self, context: TurnContext, instance: DialogInstance\n    ):\n        # Create and send an event to the skill so it can resume the dialog.\n        reprompt_event = Activity(\n            type=ActivityTypes.event, name=DialogEvents.reprompt_dialog\n        )\n\n        # Apply conversation reference and common properties from incoming activity before sending.\n        TurnContext.apply_conversation_reference(\n            reprompt_event,\n            TurnContext.get_conversation_reference(context.activity),\n            is_incoming=True,\n        )\n\n        # connection Name is not applicable for a RePrompt, as we don't expect as OAuthCard in response.\n        skill_conversation_id = instance.state[SkillDialog.SKILLCONVERSATIONIDSTATEKEY]\n        await self._send_to_skill(context, reprompt_event, skill_conversation_id)\n\n    async def resume_dialog(  # pylint: disable=unused-argument\n        self, dialog_context: \"DialogContext\", reason: DialogReason, result: object\n    ):\n        await self.reprompt_dialog(dialog_context.context, dialog_context.active_dialog)\n        return self.end_of_turn\n\n    async def end_dialog(\n        self, context: TurnContext, instance: DialogInstance, reason: DialogReason\n    ):\n        # Send of of conversation to the skill if the dialog has been cancelled.\n        if reason in (DialogReason.CancelCalled, DialogReason.ReplaceCalled):\n            await context.send_trace_activity(\n                f\"{SkillDialog.__name__}.end_dialog()\",\n                label=f\"ActivityType: {context.activity.type}\",\n            )\n            activity = Activity(type=ActivityTypes.end_of_conversation)\n\n            # Apply conversation reference and common properties from incoming activity before sending.\n            TurnContext.apply_conversation_reference(\n                activity,\n                TurnContext.get_conversation_reference(context.activity),\n                is_incoming=True,\n            )\n            activity.channel_data = context.activity.channel_data\n            activity.additional_properties = context.activity.additional_properties\n\n            # connection Name is not applicable for an EndDialog, as we don't expect as OAuthCard in response.\n            skill_conversation_id = instance.state[\n                SkillDialog.SKILLCONVERSATIONIDSTATEKEY\n            ]\n            await self._send_to_skill(context, activity, skill_conversation_id)\n\n        await super().end_dialog(context, instance, reason)\n\n    def _validate_begin_dialog_args(self, options: object) -> BeginSkillDialogOptions:\n        if not options:\n            raise TypeError(\"options cannot be None.\")\n\n        dialog_args = BeginSkillDialogOptions.from_object(options)\n\n        if not dialog_args:\n            raise TypeError(\n                \"SkillDialog: options object not valid as BeginSkillDialogOptions.\"\n            )\n\n        if not dialog_args.activity:\n            raise TypeError(\n                \"SkillDialog: activity object in options as BeginSkillDialogOptions cannot be None.\"\n            )\n\n        return dialog_args\n\n    def _on_validate_activity(\n        self, activity: Activity  # pylint: disable=unused-argument\n    ) -> bool:\n        \"\"\"\n        Validates the activity sent during continue_dialog.\n\n        Override this method to implement a custom validator for the activity being sent during continue_dialog.\n        This method can be used to ignore activities of a certain type if needed.\n        If this method returns false, the dialog will end the turn without processing the activity.\n        \"\"\"\n        return True\n\n    async def _send_to_skill(\n        self, context: TurnContext, activity: Activity, skill_conversation_id: str\n    ) -> Activity:\n        if activity.type == ActivityTypes.invoke:\n            # Force ExpectReplies for invoke activities so we can get the replies right away and send\n            # them back to the channel if needed. This makes sure that the dialog will receive the Invoke\n            # response from the skill and any other activities sent, including EoC.\n            activity.delivery_mode = DeliveryModes.expect_replies\n\n        # Always save state before forwarding\n        # (the dialog stack won't get updated with the skillDialog and things won't work if you don't)\n        await self.dialog_options.conversation_state.save_changes(context, True)\n\n        skill_info = self.dialog_options.skill\n        response = await self.dialog_options.skill_client.post_activity(\n            self.dialog_options.bot_id,\n            skill_info.app_id,\n            skill_info.skill_endpoint,\n            self.dialog_options.skill_host_endpoint,\n            skill_conversation_id,\n            activity,\n        )\n\n        # Inspect the skill response status\n        if not 200 <= response.status <= 299:\n            raise Exception(\n                f'Error invoking the skill id: \"{skill_info.id}\" at \"{skill_info.skill_endpoint}\"'\n                f\" (status is {response.status}). \\r\\n {response.body}\"\n            )\n\n        eoc_activity: Activity = None\n        if activity.delivery_mode == DeliveryModes.expect_replies and response.body:\n            # Process replies in the response.Body.\n            response.body: List[Activity]\n            response.body = ExpectedReplies().deserialize(response.body).activities\n\n            for from_skill_activity in response.body:\n                if from_skill_activity.type == ActivityTypes.end_of_conversation:\n                    # Capture the EndOfConversation activity if it was sent from skill\n                    eoc_activity = from_skill_activity\n\n                    # The conversation has ended, so cleanup the conversation id\n                    await self.dialog_options.conversation_id_factory.delete_conversation_reference(\n                        skill_conversation_id\n                    )\n                elif await self._intercept_oauth_cards(\n                    context, from_skill_activity, self.dialog_options.connection_name\n                ):\n                    # do nothing. Token exchange succeeded, so no oauthcard needs to be shown to the user\n                    pass\n                else:\n                    # Send the response back to the channel.\n                    await context.send_activity(from_skill_activity)\n\n        return eoc_activity\n\n    async def _create_skill_conversation_id(\n        self, context: TurnContext, activity: Activity\n    ) -> str:\n        # Create a conversationId to interact with the skill and send the activity\n        conversation_id_factory_options = SkillConversationIdFactoryOptions(\n            from_bot_oauth_scope=context.turn_state.get(BotAdapter.BOT_OAUTH_SCOPE_KEY),\n            from_bot_id=self.dialog_options.bot_id,\n            activity=activity,\n            bot_framework_skill=self.dialog_options.skill,\n        )\n        skill_conversation_id = await self.dialog_options.conversation_id_factory.create_skill_conversation_id(\n            conversation_id_factory_options\n        )\n        return skill_conversation_id\n\n    async def _intercept_oauth_cards(\n        self, context: TurnContext, activity: Activity, connection_name: str\n    ):\n        \"\"\"\n        Tells is if we should intercept the OAuthCard message.\n        \"\"\"\n        if not connection_name or not isinstance(\n            context.adapter, ExtendedUserTokenProvider\n        ):\n            # The adapter may choose not to support token exchange, in which case we fallback to\n            # showing an oauth card to the user.\n            return False\n\n        oauth_card_attachment = next(\n            attachment\n            for attachment in activity.attachments\n            if attachment.content_type == ContentTypes.oauth_card\n        )\n        if oauth_card_attachment:\n            oauth_card = oauth_card_attachment.content\n            if (\n                oauth_card\n                and oauth_card.token_exchange_resource\n                and oauth_card.token_exchange_resource.uri\n            ):\n                try:\n                    result = await context.adapter.exchange_token(\n                        turn_context=context,\n                        connection_name=connection_name,\n                        user_id=context.activity.from_property.id,\n                        exchange_request=TokenExchangeRequest(\n                            uri=oauth_card.token_exchange_resource.uri\n                        ),\n                    )\n\n                    if result and result.token:\n                        # If token above is null, then SSO has failed and hence we return false.\n                        # If not, send an invoke to the skill with the token.\n                        return await self._send_token_exchange_invoke_to_skill(\n                            activity,\n                            oauth_card.token_exchange_resource.id,\n                            oauth_card.connection_name,\n                            result.token,\n                        )\n                except:\n                    # Failures in token exchange are not fatal. They simply mean that the user needs\n                    # to be shown the OAuth card.\n                    return False\n\n        return False\n\n    async def _send_token_exchange_invoke_to_skill(\n        self,\n        incoming_activity: Activity,\n        request_id: str,\n        connection_name: str,\n        token: str,\n    ):\n        activity = incoming_activity.create_reply()\n        activity.type = ActivityTypes.invoke\n        activity.name = SignInConstants.token_exchange_operation_name\n        activity.value = TokenExchangeInvokeRequest(\n            id=request_id, token=token, connection_name=connection_name,\n        )\n\n        # route the activity to the skill\n        skill_info = self.dialog_options.skill\n        response = await self.dialog_options.skill_client.post_activity(\n            self.dialog_options.bot_id,\n            skill_info.app_id,\n            skill_info.skill_endpoint,\n            self.dialog_options.skill_host_endpoint,\n            incoming_activity.conversation.id,\n            activity,\n        )\n\n        # Check response status: true if success, false if failure\n        return response.is_successful_status_code()\n", "path": "libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py"}]}