Spaces:
Build error
Build error
File size: 9,061 Bytes
0827183 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
from botframework.connector.auth import (
ClaimsIdentity,
SkillValidation,
AuthenticationConstants,
GovernmentConstants,
)
from botbuilder.core import BotAdapter, StatePropertyAccessor, TurnContext
from botbuilder.core.skills import SkillHandler, SkillConversationReference
import botbuilder.dialogs as dialogs # pylint: disable=unused-import
from botbuilder.dialogs.memory import DialogStateManager
from botbuilder.dialogs.dialog_context import DialogContext
from botbuilder.dialogs.dialog_turn_result import DialogTurnResult
from botbuilder.dialogs import (
DialogEvents,
DialogSet,
DialogTurnStatus,
)
from botbuilder.schema import Activity, ActivityTypes, EndOfConversationCodes
class DialogExtensions:
@staticmethod
async def run_dialog(
dialog: "dialogs.Dialog",
turn_context: TurnContext,
accessor: StatePropertyAccessor,
):
"""
Creates a dialog stack and starts a dialog, pushing it onto the stack.
"""
dialog_set = DialogSet(accessor)
dialog_set.add(dialog)
dialog_context: DialogContext = await dialog_set.create_context(turn_context)
await DialogExtensions._internal_run(turn_context, dialog.id, dialog_context)
@staticmethod
async def _internal_run(
context: TurnContext, dialog_id: str, dialog_context: DialogContext
) -> DialogTurnResult:
# map TurnState into root dialog context.services
for key, service in context.turn_state.items():
dialog_context.services[key] = service
# get the DialogStateManager configuration
dialog_state_manager = DialogStateManager(dialog_context)
await dialog_state_manager.load_all_scopes()
dialog_context.context.turn_state[dialog_state_manager.__class__.__name__] = (
dialog_state_manager
)
# Loop as long as we are getting valid OnError handled we should continue executing the actions for the turn.
# NOTE: We loop around this block because each pass through we either complete the turn and break out of the
# loop or we have had an exception AND there was an OnError action which captured the error. We need to
# continue the turn based on the actions the OnError handler introduced.
end_of_turn = False
while not end_of_turn:
try:
dialog_turn_result = await DialogExtensions.__inner_run(
context, dialog_id, dialog_context
)
# turn successfully completed, break the loop
end_of_turn = True
except Exception as err:
# fire error event, bubbling from the leaf.
handled = await dialog_context.emit_event(
DialogEvents.error, err, bubble=True, from_leaf=True
)
if not handled:
# error was NOT handled, throw the exception and end the turn. (This will trigger the
# Adapter.OnError handler and end the entire dialog stack)
raise
# save all state scopes to their respective botState locations.
await dialog_state_manager.save_all_changes()
# return the redundant result because the DialogManager contract expects it
return dialog_turn_result
@staticmethod
async def __inner_run(
turn_context: TurnContext, dialog_id: str, dialog_context: DialogContext
) -> DialogTurnResult:
# Handle EoC and Reprompt event from a parent bot (can be root bot to skill or skill to skill)
if DialogExtensions.__is_from_parent_to_skill(turn_context):
# Handle remote cancellation request from parent.
if turn_context.activity.type == ActivityTypes.end_of_conversation:
if not dialog_context.stack:
# No dialogs to cancel, just return.
return DialogTurnResult(DialogTurnStatus.Empty)
# Send cancellation message to the dialog to ensure all the parents are canceled
# in the right order.
return await dialog_context.cancel_all_dialogs(True)
# Handle a reprompt event sent from the parent.
if (
turn_context.activity.type == ActivityTypes.event
and turn_context.activity.name == DialogEvents.reprompt_dialog
):
if not dialog_context.stack:
# No dialogs to reprompt, just return.
return DialogTurnResult(DialogTurnStatus.Empty)
await dialog_context.reprompt_dialog()
return DialogTurnResult(DialogTurnStatus.Waiting)
# Continue or start the dialog.
result = await dialog_context.continue_dialog()
if result.status == DialogTurnStatus.Empty:
result = await dialog_context.begin_dialog(dialog_id)
await DialogExtensions._send_state_snapshot_trace(dialog_context)
# Skills should send EoC when the dialog completes.
if (
result.status == DialogTurnStatus.Complete
or result.status == DialogTurnStatus.Cancelled
):
if DialogExtensions.__send_eoc_to_parent(turn_context):
activity = Activity(
type=ActivityTypes.end_of_conversation,
value=result.result,
locale=turn_context.activity.locale,
code=(
EndOfConversationCodes.completed_successfully
if result.status == DialogTurnStatus.Complete
else EndOfConversationCodes.user_cancelled
),
)
await turn_context.send_activity(activity)
return result
@staticmethod
def __is_from_parent_to_skill(turn_context: TurnContext) -> bool:
if turn_context.turn_state.get(SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY):
return False
claims_identity = turn_context.turn_state.get(BotAdapter.BOT_IDENTITY_KEY)
return isinstance(
claims_identity, ClaimsIdentity
) and SkillValidation.is_skill_claim(claims_identity.claims)
@staticmethod
async def _send_state_snapshot_trace(dialog_context: DialogContext):
"""
Helper to send a trace activity with a memory snapshot of the active dialog DC.
:param dialog_context:
:return:
"""
claims_identity = dialog_context.context.turn_state.get(
BotAdapter.BOT_IDENTITY_KEY, None
)
trace_label = (
"Skill State"
if isinstance(claims_identity, ClaimsIdentity)
and SkillValidation.is_skill_claim(claims_identity.claims)
else "Bot State"
)
# send trace of memory
snapshot = DialogExtensions._get_active_dialog_context(
dialog_context
).state.get_memory_snapshot()
trace_activity = Activity.create_trace_activity(
"BotState",
"https://www.botframework.com/schemas/botState",
snapshot,
trace_label,
)
await dialog_context.context.send_activity(trace_activity)
@staticmethod
def __send_eoc_to_parent(turn_context: TurnContext) -> bool:
claims_identity = turn_context.turn_state.get(BotAdapter.BOT_IDENTITY_KEY)
if isinstance(
claims_identity, ClaimsIdentity
) and SkillValidation.is_skill_claim(claims_identity.claims):
# EoC Activities returned by skills are bounced back to the bot by SkillHandler.
# In those cases we will have a SkillConversationReference instance in state.
skill_conversation_reference: SkillConversationReference = (
turn_context.turn_state.get(
SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY
)
)
if skill_conversation_reference:
# If the skillConversationReference.OAuthScope is for one of the supported channels,
# we are at the root and we should not send an EoC.
return (
skill_conversation_reference.oauth_scope
!= AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
and skill_conversation_reference.oauth_scope
!= GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
)
return True
return False
@staticmethod
def _get_active_dialog_context(dialog_context: DialogContext) -> DialogContext:
"""
Recursively walk up the DC stack to find the active DC.
:param dialog_context:
:return:
"""
child = dialog_context.child
if not child:
return dialog_context
return DialogExtensions._get_active_dialog_context(child)
|