Skip to content

Commit

Permalink
Doc and comment changes to match C#
Browse files Browse the repository at this point in the history
  • Loading branch information
tracyboehrer committed Apr 24, 2020
1 parent 54559a6 commit fe3a8a5
Show file tree
Hide file tree
Showing 11 changed files with 108 additions and 32 deletions.
2 changes: 1 addition & 1 deletion samples/python/81.skills-skilldialog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ It demonstrates how to post activities from the parent bot to the skill bot and
- Event activities are routed to specific dialogs using the parameters provided in the `Values` property of the activity.
- Message activities are sent to LUIS if configured and trigger the desired tasks if the intent is recognized.
- A sample [ActivityHandler](dialog-skill-bot/bots/skill_bot.py) that uses the `run_dialog` method on `DialogExtensions`.

Note: Starting in Bot Framework 4.8, the `DialogExtensions` class was introduced to provide a `run_dialog` method wich adds support to automatically send `EndOfConversation` with return values when the bot is running as a skill and the current dialog ends. It also handles reprompt messages to resume a skill where it left of.
- A sample [SkillAdapterWithErrorHandler](dialog-skill-bot/skill_adapter_with_error_handler.py) adapter that shows how to handle errors, terminate the skills, send traces back to the emulator to help debugging the bot and send `EndOfConversation` messages to the parent bot with details of the error.
- A sample [AllowedCallersClaimsValidator](dialog-skill-bot/authentication/allow_callers_claims_validation.py) that shows how to validate that the skill is only invoked from a list of allowed callers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async def _send_error_message(self, turn_context: TurnContext, error: Exception)
return
try:
# Send a message to the user.
error_message_text = "The skill encountered an error or bug."
error_message_text = "The bot encountered an error or bug."
error_message = MessageFactory.text(
error_message_text, error_message_text, InputHints.ignoring_input
)
Expand Down Expand Up @@ -85,7 +85,7 @@ async def _send_error_message(self, turn_context: TurnContext, error: Exception)
traceback.print_exc()

async def _end_skill_conversation(
self, turn_context: TurnContext, error: Exception
self, turn_context: TurnContext, error: Exception # pylint: disable=unused-argument
):
if not self._skill_client or not self._skill_config:
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ def __init__(

async def on_turn(self, turn_context: TurnContext):
if turn_context.activity.type != ActivityTypes.conversation_update:
# Handle end of conversation back from the skill
# forget skill invocation
# Run the Dialog with the Activity.
await DialogExtensions.run_dialog(
self._main_dialog,
turn_context,
Expand Down Expand Up @@ -54,6 +53,10 @@ async def on_members_added_activity(
)

def _create_adaptive_card_attachment(self) -> Attachment:
"""
Load attachment from embedded resource.
"""

relative_path = os.path.abspath(os.path.dirname(__file__))
path = os.path.join(relative_path, "../cards/welcomeCard.json")
with open(path) as in_file:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@


class MainDialog(ComponentDialog):
"""
The main dialog for this bot. It uses a SkillDialog to call skills.
"""

ACTIVE_SKILL_PROPERTY_NAME = f"MainDialog.ActiveSkillProperty"

Expand Down Expand Up @@ -78,15 +81,15 @@ def __init__(
bot_id,
)

# ChoicePrompt to render available skills
# Add ChoicePrompt to render available skills.
self.add_dialog(ChoicePrompt("SkillPrompt"))

# ChoicePrompt to render skill actions
# Add ChoicePrompt to render skill actions.
self.add_dialog(
ChoicePrompt("SkillActionPrompt", self._skill_action_prompt_validator)
)

# Main waterfall dialog for this bot
# Add main waterfall dialog for this bot.
self.add_dialog(
WaterfallDialog(
WaterfallDialog.__name__,
Expand All @@ -99,14 +102,16 @@ def __init__(
)
)

# Create state property to track the active skill.
self._active_skill_property = conversation_state.create_property(
MainDialog.ACTIVE_SKILL_PROPERTY_NAME
)

# The initial child Dialog to run.
self.initial_dialog_id = WaterfallDialog.__name__

async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult:
# This is an example on how to cancel a SkillDialog that is currently in progress from the parent bot
# This is an example on how to cancel a SkillDialog that is currently in progress from the parent bot.
active_skill = await self._active_skill_property.get(inner_dc.context)
activity = inner_dc.context.activity

Expand All @@ -115,7 +120,9 @@ async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult:
and activity.type == ActivityTypes.message
and "abort" in activity.text
):
# Cancel all dialog when the user says abort.
# Cancel all dialogs when the user says abort.
# The SkillDialog automatically sends an EndOfConversation message to the skill to let the
# skill know that it needs to end its current dialogs, too.
await inner_dc.cancel_all_dialogs()
return await inner_dc.replace_dialog(self.initial_dialog_id)

Expand All @@ -124,6 +131,10 @@ async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult:
async def _select_skill_step(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
"""
Render a prompt to select the skill to call.
"""

# Create the PromptOptions from the skill configuration which contain the list of configured skills.
message_text = (
str(step_context.options)
Expand All @@ -146,6 +157,10 @@ async def _select_skill_step(
async def _select_skill_action_step(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
"""
Render a prompt to select the action for the skill.
"""

# Get the skill info based on the selected skill.
selected_skill_id = step_context.result.value
selected_skill = self._skills_config.SKILLS.get(selected_skill_id)
Expand All @@ -154,7 +169,10 @@ async def _select_skill_action_step(
step_context.values[self._selected_skill_key] = selected_skill

# Create the PromptOptions with the actions supported by the selected skill.
message_text = f"Select an action # to send to **{selected_skill.id}** or just type in a message and it will be forwarded to the skill"
message_text = (
f"Select an action # to send to **{selected_skill.id}** or just type in a message "
f"and it will be forwarded to the skill"
)
options = PromptOptions(
prompt=MessageFactory.text(
message_text, message_text, InputHints.expecting_input
Expand All @@ -168,7 +186,10 @@ async def _select_skill_action_step(
async def _call_skill_action_step(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
# Starts SkillDialog based on the user's selections
"""
Starts the SkillDialog based on the user's selections.
"""

selected_skill: BotFrameworkSkill = step_context.values[
self._selected_skill_key
]
Expand All @@ -190,6 +211,10 @@ async def _call_skill_action_step(
return await step_context.begin_dialog(selected_skill.id, skill_dialog_args)

async def _final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
"""
The SkillDialog has ended, render the results (if any) and restart MainDialog.
"""

active_skill = await self._active_skill_property.get(step_context.context)

if step_context.result:
Expand All @@ -202,7 +227,7 @@ async def _final_step(self, step_context: WaterfallStepContext) -> DialogTurnRes
# Clear the skill selected by the user.
step_context.values[self._selected_skill_key] = None

# Clear active skill in state
# Clear active skill in state.
await self._active_skill_property.delete(step_context.context)

# Restart the main dialog with a different message the second time around
Expand All @@ -211,7 +236,6 @@ async def _final_step(self, step_context: WaterfallStepContext) -> DialogTurnRes
f'Done with "{active_skill.id}". \n\n What skill would you like to call?',
)

# Helper method that creates and adds SkillDialog instances for the configured skills.
def _add_skill_dialogs(
self,
conversation_state: ConversationState,
Expand All @@ -220,6 +244,10 @@ def _add_skill_dialogs(
skills_config: SkillConfiguration,
bot_id: str,
):
"""
Helper method that creates and adds SkillDialog instances for the configured skills.
"""

for _, skill_info in skills_config.SKILLS.items():
# Create the dialog options.
skill_dialog_options = SkillDialogOptions(
Expand All @@ -234,10 +262,13 @@ def _add_skill_dialogs(
# Add a SkillDialog for the selected skill.
self.add_dialog(SkillDialog(skill_dialog_options, skill_info.id))

# This validator defaults to Message if the user doesn't select an existing option.
async def _skill_action_prompt_validator(
self, prompt_context: PromptValidatorContext
) -> bool:
"""
This validator defaults to Message if the user doesn't select an existing option.
"""

if not prompt_context.recognized.succeeded:
# Assume the user wants to send a message if an item in the list is not selected.
prompt_context.recognized.value = FoundChoice(
Expand All @@ -246,8 +277,11 @@ async def _skill_action_prompt_validator(

return True

# Helper method to create Choice elements for the actions supported by the skill.
def _get_skill_actions(self, skill: BotFrameworkSkill) -> List[Choice]:
"""
Helper method to create Choice elements for the actions supported by the skill.
"""

# Note: the bot would probably render this by reading the skill manifest.
# We are just using hardcoded skill actions here for simplicity.

Expand All @@ -259,10 +293,13 @@ def _get_skill_actions(self, skill: BotFrameworkSkill) -> List[Choice]:

return choices

# Helper method to create the activity to be sent to the DialogSkillBot using selected type and values.
def _create_dialog_skill_bot_activity(
self, selected_option: str, turn_context: TurnContext
) -> Activity:
"""
Helper method to create the activity to be sent to the DialogSkillBot using selected type and values.
"""

selected_option = selected_option.lower()
# Note: in a real bot, the dialogArgs will be created dynamically based on the conversation
# and what each action requires; here we hardcode the values to make things simpler.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from typing import Awaitable, Callable, List

from botbuilder.core import Middleware, TurnContext
from botbuilder.schema import Activity, ResourceResponse
from botbuilder.schema import Activity, ResourceResponse, ActivityTypes


class LoggerMiddleware(Middleware):
"""
Uses an ILogger instance to log user and bot messages. It filters out ContinueConversation
events coming from skill responses.
"""

def __init__(self, label: str = None):
self._label = label or LoggerMiddleware.__name__

async def on_turn(
self, context: TurnContext, logic: Callable[[TurnContext], Awaitable]
):
message = f"{self._label} {context.activity.type} {context.activity.text}"
print(message)
# Note: skill responses will show as ContinueConversation events; we don't log those.
# We only log incoming messages from users.
if (
context.activity.type != ActivityTypes.event
and context.activity.name != "ContinueConversation"
):
message = f"{self._label} {context.activity.type} {context.activity.text}"
print(message)

# Register outgoing handler
# Register outgoing handler.
context.on_send_activities(self._outgoing_handler)

# Continue processing messages.
await logic()

async def _outgoing_handler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@


class AllowedCallersClaimsValidator:
"""
Sample claims validator that loads an allowed list from configuration if present
and checks that requests are coming from allowed parent bots.
"""

config_key = "ALLOWED_CALLERS"

Expand All @@ -15,10 +19,11 @@ def __init__(self, config: DefaultConfig):
"AllowedCallersClaimsValidator: config object cannot be None."
)

# ALLOWED_CALLERS is the setting in config.py file
# that consists of the list of parent bot ids that are allowed to access the skill
# to add a new parent bot simply go to the AllowedCallers and add
# the parent bot's microsoft app id to the list
# AllowedCallers is the setting in the appsettings.json file
# that consists of the list of parent bot IDs that are allowed to access the skill.
# To add a new parent bot, simply edit the AllowedCallers and add
# the parent bot's Microsoft app ID to the list.
# In this sample, we allow all callers if AllowedCallers contains an "*".
caller_list = getattr(config, self.config_key)
if caller_list is None:
raise TypeError(f'"{self.config_key}" not found in configuration.')
Expand All @@ -27,11 +32,11 @@ def __init__(self, config: DefaultConfig):
@property
def claims_validator(self) -> Callable[[List[Dict]], Awaitable]:
async def allow_callers_claims_validator(claims: Dict[str, object]):
# if allowed_callers is None we allow all calls
# If _allowed_callers contains an "*", we allow all callers.
if "*" not in self._allowed_callers and SkillValidation.is_skill_claim(
claims
):
# Check that the appId claim in the skill request is in the list of skills configured for this bot.
# Check that the appId claim in the skill request is in the list of callers configured for this bot.
app_id = JwtTokenValidation.get_app_id_from_claims(claims)
if app_id not in self._allowed_callers:
raise PermissionError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ async def process_activity(
async def _on_event_activity(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
"""
This method performs different tasks based on the event name.
"""

activity = step_context.context.activity

# Resolve what to execute based on the event name.
Expand All @@ -84,6 +88,10 @@ async def _on_event_activity(
async def _on_message_activity(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
"""
This method just gets a message activity and runs it through LUIS.
"""

activity = step_context.context.activity

if not self._luis_recognizer.is_configured:
Expand Down Expand Up @@ -121,6 +129,7 @@ async def _on_message_activity(
if top_intent == "GetWeather":
return await self._begin_get_weather(step_context)

# Catch all for unhandled intents.
didnt_understand_message_text = f"Sorry, I didn't get that. Please try asking in a different way (intent was {top_intent})"
await step_context.context.send_activity(
MessageFactory.text(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult:
if inner_dc.context.activity.type == ActivityTypes.message:
text = inner_dc.context.activity.text.lower()

help_message_text = "Show Help..."
help_message_text = "Show help here"
help_message = MessageFactory.text(
help_message_text, help_message_text, InputHints.expecting_input
)
Expand All @@ -35,7 +35,7 @@ async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult:
await inner_dc.context.send_activity(help_message)
return DialogTurnResult(DialogTurnStatus.Waiting)

cancel_message_text = "Cancelling"
cancel_message_text = "Canceling..."
cancel_message = MessageFactory.text(
cancel_message_text, cancel_message_text, InputHints.ignoring_input
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,14 @@ async def final_step(self, step_context: WaterfallStepContext):
@staticmethod
async def datetime_prompt_validator(prompt_context: PromptValidatorContext) -> bool:
if prompt_context.recognized.succeeded:
# This value will be a TIMEX. We are only interested in the Date part, so grab the first
# result and drop the Time part.
# TIMEX is a format that represents DateTime expressions that include some ambiguity,
# such as a missing Year.
timex = prompt_context.recognized.value[0].timex.split("T")[0]

# TODO: Needs TimexProperty
# If this is a definite Date that includes year, month and day we are good; otherwise, reprompt.
# A better solution might be to let the user know what part is actually missing.
return "definite" in Timex(timex).types

return False
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def __init__(self, configuration: DefaultConfig):

@property
def is_configured(self) -> bool:
# Returns true if luis is configured in the config.py and initialized.
# Returns true if LUIS is configured in config.py and initialized.
return self._recognizer is not None

async def recognize(self, turn_context: TurnContext) -> RecognizerResult:
Expand Down
Loading

0 comments on commit fe3a8a5

Please sign in to comment.