Spaces:
Build error
Build error
File size: 12,678 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 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 |
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
from datetime import datetime, timedelta
from threading import Lock
from warnings import warn
from botbuilder.core import (
BotAdapter,
BotStateSet,
ConversationState,
UserState,
TurnContext,
)
from botbuilder.core.skills import SkillConversationReference, SkillHandler
from botbuilder.dialogs.memory import DialogStateManagerConfiguration
from botbuilder.schema import Activity, ActivityTypes, EndOfConversationCodes
from botframework.connector.auth import (
AuthenticationConstants,
ClaimsIdentity,
GovernmentConstants,
SkillValidation,
)
from .dialog import Dialog
from .dialog_context import DialogContext
from .dialog_events import DialogEvents
from .dialog_extensions import DialogExtensions
from .dialog_set import DialogSet
from .dialog_state import DialogState
from .dialog_manager_result import DialogManagerResult
from .dialog_turn_status import DialogTurnStatus
from .dialog_turn_result import DialogTurnResult
class DialogManager:
"""
Class which runs the dialog system.
"""
def __init__(self, root_dialog: Dialog = None, dialog_state_property: str = None):
"""
Initializes a instance of the <see cref="DialogManager"/> class.
:param root_dialog: Root dialog to use.
:param dialog_state_property: alternate name for the dialog_state property. (Default is "DialogState").
"""
self.last_access = "_lastAccess"
self._root_dialog_id = ""
self._dialog_state_property = dialog_state_property or "DialogState"
self._lock = Lock()
# Gets or sets root dialog to use to start conversation.
self.root_dialog = root_dialog
# Gets or sets the ConversationState.
self.conversation_state: ConversationState = None
# Gets or sets the UserState.
self.user_state: UserState = None
# Gets InitialTurnState collection to copy into the TurnState on every turn.
self.initial_turn_state = {}
# Gets or sets global dialogs that you want to have be callable.
self.dialogs = DialogSet()
# Gets or sets the DialogStateManagerConfiguration.
self.state_configuration: DialogStateManagerConfiguration = None
# Gets or sets (optional) number of milliseconds to expire the bot's state after.
self.expire_after: int = None
async def on_turn(self, context: TurnContext) -> DialogManagerResult:
"""
Runs dialog system in the context of an ITurnContext.
:param context: turn context.
:return:
"""
# pylint: disable=too-many-statements
# Lazy initialize RootDialog so it can refer to assets like LG function templates
if not self._root_dialog_id:
with self._lock:
if not self._root_dialog_id:
self._root_dialog_id = self.root_dialog.id
# self.dialogs = self.root_dialog.telemetry_client
self.dialogs.add(self.root_dialog)
bot_state_set = BotStateSet([])
# Preload TurnState with DM TurnState.
for key, val in self.initial_turn_state.items():
context.turn_state[key] = val
# register DialogManager with TurnState.
context.turn_state[DialogManager.__name__] = self
conversation_state_name = ConversationState.__name__
if self.conversation_state is None:
if conversation_state_name not in context.turn_state:
raise Exception(
f"Unable to get an instance of {conversation_state_name} from turn_context."
)
self.conversation_state: ConversationState = context.turn_state[
conversation_state_name
]
else:
context.turn_state[conversation_state_name] = self.conversation_state
bot_state_set.add(self.conversation_state)
user_state_name = UserState.__name__
if self.user_state is None:
self.user_state = context.turn_state.get(user_state_name, None)
else:
context.turn_state[user_state_name] = self.user_state
if self.user_state is not None:
self.user_state: UserState = self.user_state
bot_state_set.add(self.user_state)
# create property accessors
# DateTime(last_access)
last_access_property = self.conversation_state.create_property(self.last_access)
last_access: datetime = await last_access_property.get(context, datetime.now)
# Check for expired conversation
if self.expire_after is not None and (
datetime.now() - last_access
) >= timedelta(milliseconds=float(self.expire_after)):
# Clear conversation state
await self.conversation_state.clear_state(context)
last_access = datetime.now()
await last_access_property.set(context, last_access)
# get dialog stack
dialogs_property = self.conversation_state.create_property(
self._dialog_state_property
)
dialog_state: DialogState = await dialogs_property.get(context, DialogState)
# Create DialogContext
dialog_context = DialogContext(self.dialogs, context, dialog_state)
# Call the common dialog "continue/begin" execution pattern shared with the classic RunAsync extension method
turn_result = (
await DialogExtensions._internal_run( # pylint: disable=protected-access
context, self._root_dialog_id, dialog_context
)
)
# save BotState changes
await bot_state_set.save_all_changes(dialog_context.context, False)
return DialogManagerResult(turn_result=turn_result)
@staticmethod
async def send_state_snapshot_trace(
dialog_context: DialogContext,
trace_label: str = None, # pylint: disable=unused-argument
):
"""
Helper to send a trace activity with a memory snapshot of the active dialog DC.
:param dialog_context:
:param trace_label:
:return:
"""
warn(
"This method will be deprecated as no longer is necesary",
PendingDeprecationWarning,
)
await DialogExtensions._send_state_snapshot_trace( # pylint: disable=protected-access
dialog_context
)
@staticmethod
def is_from_parent_to_skill(turn_context: TurnContext) -> bool:
if turn_context.turn_state.get(
SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY, None
):
return False
claims_identity: ClaimsIdentity = turn_context.turn_state.get(
BotAdapter.BOT_IDENTITY_KEY, None
)
return isinstance(
claims_identity, ClaimsIdentity
) and SkillValidation.is_skill_claim(claims_identity.claims)
# Recursively walk up the DC stack to find the active DC.
@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:
"""
warn(
"This method will be deprecated as no longer is necesary",
PendingDeprecationWarning,
)
return DialogExtensions._get_active_dialog_context( # pylint: disable=protected-access
dialog_context
)
@staticmethod
def should_send_end_of_conversation_to_parent(
context: TurnContext, turn_result: DialogTurnResult
) -> bool:
"""
Helper to determine if we should send an EndOfConversation to the parent or not.
:param context:
:param turn_result:
:return:
"""
if not (
turn_result.status == DialogTurnStatus.Complete
or turn_result.status == DialogTurnStatus.Cancelled
):
# The dialog is still going, don't return EoC.
return False
claims_identity: ClaimsIdentity = context.turn_state.get(
BotAdapter.BOT_IDENTITY_KEY, None
)
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 = (
context.turn_state.get(SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY)
)
if skill_conversation_reference:
# If the skill_conversation_reference.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 not in (
AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
)
return True
return False
async def handle_skill_on_turn(
self, dialog_context: DialogContext
) -> DialogTurnResult:
warn(
"This method will be deprecated as no longer is necesary",
PendingDeprecationWarning,
)
# the bot is running as a skill.
turn_context = dialog_context.context
# Process remote cancellation
if (
turn_context.activity.type == ActivityTypes.end_of_conversation
and dialog_context.active_dialog is not None
and self.is_from_parent_to_skill(turn_context)
):
# Handle remote cancellation request from parent.
active_dialog_context = self.get_active_dialog_context(dialog_context)
# Send cancellation message to the top dialog in the stack to ensure all the parents are canceled in the
# right order.
return await active_dialog_context.cancel_all_dialogs(True)
# Handle reprompt
# Process 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.active_dialog:
return DialogTurnResult(DialogTurnStatus.Empty)
await dialog_context.reprompt_dialog()
return DialogTurnResult(DialogTurnStatus.Waiting)
# Continue execution
# - This will apply any queued up interruptions and execute the current/next step(s).
turn_result = await dialog_context.continue_dialog()
if turn_result.status == DialogTurnStatus.Empty:
# restart root dialog
turn_result = await dialog_context.begin_dialog(self._root_dialog_id)
await DialogManager.send_state_snapshot_trace(dialog_context, "Skill State")
if self.should_send_end_of_conversation_to_parent(turn_context, turn_result):
# Send End of conversation at the end.
activity = Activity(
type=ActivityTypes.end_of_conversation,
value=turn_result.result,
locale=turn_context.activity.locale,
code=(
EndOfConversationCodes.completed_successfully
if turn_result.status == DialogTurnStatus.Complete
else EndOfConversationCodes.user_cancelled
),
)
await turn_context.send_activity(activity)
return turn_result
async def handle_bot_on_turn(
self, dialog_context: DialogContext
) -> DialogTurnResult:
warn(
"This method will be deprecated as no longer is necesary",
PendingDeprecationWarning,
)
# the bot is running as a root bot.
if dialog_context.active_dialog is None:
# start root dialog
turn_result = await dialog_context.begin_dialog(self._root_dialog_id)
else:
# Continue execution
# - This will apply any queued up interruptions and execute the current/next step(s).
turn_result = await dialog_context.continue_dialog()
if turn_result.status == DialogTurnStatus.Empty:
# restart root dialog
turn_result = await dialog_context.begin_dialog(self._root_dialog_id)
await self.send_state_snapshot_trace(dialog_context, "Bot State")
return turn_result
|