Spaces:
Build error
Build error
Validify-testbot-1
/
botbuilder-python
/libraries
/botbuilder-dialogs
/botbuilder
/dialogs
/dialog_extensions.py
# 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: | |
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) | |
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 | |
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 | |
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) | |
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) | |
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 | |
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) | |