From fecc2f1a8b8fe062a7551170e0be7cb617645895 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Wed, 12 Feb 2020 18:45:13 -0800 Subject: [PATCH 01/38] initial changes for skill dialog --- .../botbuilder/core/activity_handler.py | 1 + .../core/skills/bot_framework_client.py | 19 ++++++++ .../botbuilder/dialogs/skills/__init__.py | 17 +++++++ .../botbuilder/dialogs/skills/skill_dialog.py | 46 +++++++++++++++++++ .../dialogs/skills/skill_dialog_args.py | 11 +++++ .../dialogs/skills/skill_dialog_options.py | 18 ++++++++ 6 files changed, 112 insertions(+) create mode 100644 libraries/botbuilder-core/botbuilder/core/skills/bot_framework_client.py create mode 100644 libraries/botbuilder-dialogs/botbuilder/dialogs/skills/__init__.py create mode 100644 libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py create mode 100644 libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_args.py create mode 100644 libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_options.py diff --git a/libraries/botbuilder-core/botbuilder/core/activity_handler.py b/libraries/botbuilder-core/botbuilder/core/activity_handler.py index cecab9205..f584b489d 100644 --- a/libraries/botbuilder-core/botbuilder/core/activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/activity_handler.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + from typing import List from botbuilder.schema import ActivityTypes, ChannelAccount, MessageReaction diff --git a/libraries/botbuilder-core/botbuilder/core/skills/bot_framework_client.py b/libraries/botbuilder-core/botbuilder/core/skills/bot_framework_client.py new file mode 100644 index 000000000..d5f9e7707 --- /dev/null +++ b/libraries/botbuilder-core/botbuilder/core/skills/bot_framework_client.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from abc import ABC + +from botbuilder.schema import Activity, InvokeResponse + + +class BotFrameworkClient(ABC): + def post_activity( + self, + from_bot_id: str, + to_bot_id: str, + to_url: str, + service_url: str, + conversation_id: str, + activity: Activity, + ) -> InvokeResponse: + raise NotImplementedError() diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/__init__.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/__init__.py new file mode 100644 index 000000000..af1d0640b --- /dev/null +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/__init__.py @@ -0,0 +1,17 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +from .skill_dialog_args import SkillDialogArgs +from .skill_dialog_options import SkillDialogOptions +from .skill_dialog import SkillDialog + + +__all__ = [ + "SkillDialogArgs", + "SkillDialogOptions", + "SkillDialog", +] diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py new file mode 100644 index 000000000..dc65e94fd --- /dev/null +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core import ConversationState, StatePropertyAccessor +from botbuilder.core.skills import BotFrameworkSkill + +from botbuilder.dialogs import Dialog, DialogContext + +from .skill_dialog_args import SkillDialogArgs +from .skill_dialog_options import SkillDialogOptions + + +class SkillDialog(Dialog): + def __init__( + self, dialog_options: SkillDialogOptions, conversation_state: ConversationState + ): + super().__init__(SkillDialog.__name__) + if not dialog_options: + raise TypeError("SkillDialog.__init__(): dialog_options cannot be None.") + if not conversation_state: + raise TypeError( + "SkillDialog.__init__(): conversation_state cannot be None." + ) + self._dialog_options = dialog_options + self._conversation_state = conversation_state + self._active_skill_property: StatePropertyAccessor = conversation_state.create_property( + f"{SkillDialog.__module__}.{SkillDialog.__name__}.ActiveSkillProperty" + ) + + async def begin_dialog(self, dialog_context: DialogContext, options: object = None): + """ + Method called when a new dialog has been pushed onto the stack and is being activated. + :param dialog_context: The dialog context for the current turn of conversation. + :param options: (Optional) additional argument(s) to pass to the dialog being started. + """ + dialog_args = None + + @staticmethod + def _validate_begin_dialog_options(options: object) -> SkillDialogArgs: + if not options: + raise TypeError("options cannot be None.") + + if isinstance(options, dict): + pass + elif hasattr(options, ""): + pass diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_args.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_args.py new file mode 100644 index 000000000..0e127053a --- /dev/null +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_args.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.schema import Activity +from botbuilder.core.skills import BotFrameworkSkill + + +class SkillDialogArgs: + def __init__(self, skill: BotFrameworkSkill = None, activity: Activity = None): + self.skill = skill + self.activity = activity diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_options.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_options.py new file mode 100644 index 000000000..369ead801 --- /dev/null +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_options.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core.skills import BotFrameworkClient, SkillConversationIdFactory + + +class SkillDialogOptions: + def __init__( + self, + bot_id: str = None, + skill_client: BotFrameworkClient = None, + skill_host_endpoint: str = None, + conversation_id_factory: SkillConversationIdFactory = None, + ): + self.bot_id = bot_id + self.skill_client = skill_client + self.skill_host_endpoint = skill_host_endpoint + self.conversation_id_factory = conversation_id_factory From 2eec59497b48181f8c2750643c5a650bbfcde92e Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 14 Feb 2020 15:17:33 -0800 Subject: [PATCH 02/38] Skill dialog --- .../botbuilder/core/skills/__init__.py | 2 + .../botbuilder/dialogs/skills/skill_dialog.py | 88 +++++++++++++++++-- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/skills/__init__.py b/libraries/botbuilder-core/botbuilder/core/skills/__init__.py index 6bd5a66b8..447869029 100644 --- a/libraries/botbuilder-core/botbuilder/core/skills/__init__.py +++ b/libraries/botbuilder-core/botbuilder/core/skills/__init__.py @@ -6,12 +6,14 @@ # -------------------------------------------------------------------------- from .bot_framework_skill import BotFrameworkSkill +from .bot_framework_client import BotFrameworkClient from .conversation_id_factory import ConversationIdFactoryBase from .skill_conversation_id_factory import SkillConversationIdFactory from .skill_handler import SkillHandler __all__ = [ "BotFrameworkSkill", + "BotFrameworkClient", "ConversationIdFactoryBase", "SkillConversationIdFactory", "SkillHandler", diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py index dc65e94fd..bfde4336a 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -1,7 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from botbuilder.core import ConversationState, StatePropertyAccessor +from copy import deepcopy + +from botbuilder.schema import Activity, ActivityTypes +from botbuilder.core import ConversationState, StatePropertyAccessor, TurnContext from botbuilder.core.skills import BotFrameworkSkill from botbuilder.dialogs import Dialog, DialogContext @@ -33,14 +36,87 @@ async def begin_dialog(self, dialog_context: DialogContext, options: object = No :param dialog_context: The dialog context for the current turn of conversation. :param options: (Optional) additional argument(s) to pass to the dialog being started. """ - dialog_args = None + dialog_args = SkillDialog._validate_begin_dialog_options(options) + + await dialog_context.context.trace_activity( + f"{SkillDialog.__name__}.BeginDialogAsync()", + label=f"Using activity of type: {dialog_args.activity.type}", + ) + + # Create deep clone of the original activity to avoid altering it before forwarding it. + skill_activity: Activity = deepcopy(dialog_args.activity) + + # Apply conversation reference and common properties from incoming activity before sending. + TurnContext.apply_conversation_reference( + TurnContext.get_conversation_reference(dialog_context.context.activity), + True + ) + + # Send the activity to the skill. + await self._send_to_skill( + dialog_context, skill_activity, dialog_args.Skill + ) + return Dialog.end_of_turn @staticmethod def _validate_begin_dialog_options(options: object) -> SkillDialogArgs: if not options: raise TypeError("options cannot be None.") - if isinstance(options, dict): - pass - elif hasattr(options, ""): - pass + if isinstance(options, dict) and "skill" in options and "activity" in options: + skill_args = SkillDialogArgs( + skill=options["skill"], activity=options["skill"] + ) + elif hasattr(options, "skill") and hasattr(options, "activity"): + skill_args = SkillDialogArgs( + skill=options["skill"], activity=options["skill"] + ) + else: + raise TypeError("SkillDialog: options object not valid as SkillDialogArgs.") + + if not skill_args.activity: + raise TypeError( + "SkillDialog: activity object in options as SkillDialogArgs cannot be None." + ) + + # Only accept Message or Event activities + if ( + skill_args.activity.type != ActivityTypes.message + and skill_args.activity.type != ActivityTypes.event + ): + raise TypeError( + f"Only {ActivityTypes.message} and {ActivityTypes.event} activities are supported." + f" Received activity of type {skill_args.activity.type}." + ) + + return skill_args + + async def _send_to_skill( + self, + dialog_context: DialogContext, + activity: Activity, + skill_info: BotFrameworkSkill, + ): + # Always save state before forwarding + # (the dialog stack won't get updated with the skillDialog and things won't work if you don't) + await self._conversation_state.save_changes(dialog_context.context, True) + + # Create a conversation_id to interact with the skill and send the activity + skill_conversation_id = await self._dialog_options.conversation_id_factory.create_skill_conversation_id( + TurnContext.get_conversation_reference(activity) + ) + response = await self._dialog_options.skill_client.post_activity( + self._dialog_options.bot_id, + skill_info.app_id, + skill_info.skill_endpoint, + self._dialog_options.skill_host_endpoint, + skill_conversation_id, + activity, + ) + + # Inspect the skill response status + if not (200 <= response.status <= 299): + raise Exception( + f'Error invoking the skill id: "{skill_info.id}" at "{skill_info.skill_endpoint}"' + f" (status is {response.status}). \r\n {response.body}" + ) From 800835ba7f8747df9405c8ba2e04f7be143a86d8 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Tue, 18 Feb 2020 18:07:19 -0800 Subject: [PATCH 03/38] Initial echo skill with dialog --- .../core/bot_framework_http_client.py | 3 +- .../core/skills/bot_framework_client.py | 3 +- .../botbuilder/dialogs/skills/skill_dialog.py | 6 +- .../dialog-echo-skill-bot/README.md | 38 +++ .../dialog-echo-skill-bot/app.py | 63 +++++ .../authentication/__init__.py | 5 + .../allow_callers_claims_validation.py | 39 +++ .../dialog-echo-skill-bot/bots/__init__.py | 6 + .../dialog-echo-skill-bot/bots/echo_bot.py | 19 ++ .../dialog-echo-skill-bot/config.py | 15 + .../template-with-new-rg.json | 264 ++++++++++++++++++ .../template-with-preexisting-rg.json | 242 ++++++++++++++++ .../dialog-echo-skill-bot/requirements.txt | 2 + .../skill_adapter_with_error_handler.py | 86 ++++++ 14 files changed, 785 insertions(+), 6 deletions(-) create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/README.md create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/__init__.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/allow_callers_claims_validation.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/__init__.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/echo_bot.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-new-rg.json create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-preexisting-rg.json create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/requirements.txt create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/skill_adapter_with_error_handler.py diff --git a/libraries/botbuilder-core/botbuilder/core/bot_framework_http_client.py b/libraries/botbuilder-core/botbuilder/core/bot_framework_http_client.py index a72e3a8f5..6ee6a87f3 100644 --- a/libraries/botbuilder-core/botbuilder/core/bot_framework_http_client.py +++ b/libraries/botbuilder-core/botbuilder/core/bot_framework_http_client.py @@ -15,9 +15,10 @@ ) from . import InvokeResponse +from .skills.bot_framework_client import BotFrameworkClient -class BotFrameworkHttpClient: +class BotFrameworkHttpClient(BotFrameworkClient): """ A skill host adapter implements API to forward activity to a skill and diff --git a/libraries/botbuilder-core/botbuilder/core/skills/bot_framework_client.py b/libraries/botbuilder-core/botbuilder/core/skills/bot_framework_client.py index d5f9e7707..5213aba70 100644 --- a/libraries/botbuilder-core/botbuilder/core/skills/bot_framework_client.py +++ b/libraries/botbuilder-core/botbuilder/core/skills/bot_framework_client.py @@ -3,7 +3,8 @@ from abc import ABC -from botbuilder.schema import Activity, InvokeResponse +from botbuilder.schema import Activity +from botbuilder.core import InvokeResponse class BotFrameworkClient(ABC): diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py index bfde4336a..79a451dd7 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -49,13 +49,11 @@ async def begin_dialog(self, dialog_context: DialogContext, options: object = No # Apply conversation reference and common properties from incoming activity before sending. TurnContext.apply_conversation_reference( TurnContext.get_conversation_reference(dialog_context.context.activity), - True + True, ) # Send the activity to the skill. - await self._send_to_skill( - dialog_context, skill_activity, dialog_args.Skill - ) + await self._send_to_skill(dialog_context, skill_activity, dialog_args.Skill) return Dialog.end_of_turn @staticmethod diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/README.md b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/README.md new file mode 100644 index 000000000..299259d5b --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/README.md @@ -0,0 +1,38 @@ +# EchoBot + +Bot Framework v4 echo bot sample. + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back. + +## Running the sample +- Clone the repository +```bash +git clone https://github.com/Microsoft/botbuilder-samples.git +``` +- Bring up a terminal, navigate to `botbuilder-samples\samples\python\02.echo-bot` folder +- Activate your desired virtual environment +- In the terminal, type `pip install -r requirements.txt` +- Run your bot with `python app.py` + +## Testing the bot using Bot Framework Emulator +[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + +- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + +### Connect to bot using Bot Framework Emulator +- Launch Bot Framework Emulator +- Paste this URL in the emulator window - http://localhost:3978/api/messages + +## Deploy the bot to Azure + +To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions. + +# Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) +- [Azure Portal](https://portal.azure.com) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py new file mode 100644 index 000000000..4d4ee3c51 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py @@ -0,0 +1,63 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import sys +import traceback +from datetime import datetime + +from aiohttp import web +from aiohttp.web import Request, Response, json_response +from botbuilder.core import ( + BotFrameworkAdapterSettings, + TurnContext +) +from botbuilder.core.integration import aiohttp_error_middleware +from botbuilder.schema import Activity, ActivityTypes +from botframework.connector.auth import AuthenticationConfiguration + +from authentication import AllowedCallersClaimsValidator +from bots import EchoBot +from config import DefaultConfig +from skill_adapter_with_error_handler import SkillAdapterWithErrorHandler + +CONFIG = DefaultConfig() + +# Create adapter. +# See https://aka.ms/about-bot-adapter to learn more about how bots work. +VALIDATOR = AllowedCallersClaimsValidator(CONFIG).claims_validator +SETTINGS = BotFrameworkAdapterSettings( + CONFIG.APP_ID, + CONFIG.APP_PASSWORD, + auth_configuration=AuthenticationConfiguration(claims_validator=VALIDATOR) +) +ADAPTER = SkillAdapterWithErrorHandler(SETTINGS) + +# Create the Bot +BOT = EchoBot() + + +# Listen for incoming requests on /api/messages +async def messages(req: Request) -> Response: + # Main bot message handler. + if "application/json" in req.headers["Content-Type"]: + body = await req.json() + else: + return Response(status=415) + + activity = Activity().deserialize(body) + auth_header = req.headers["Authorization"] if "Authorization" in req.headers else "" + + response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn) + if response: + return json_response(data=response.body, status=response.status) + return Response(status=201) + + +APP = web.Application(middlewares=[aiohttp_error_middleware]) +APP.router.add_post("/api/messages", messages) + +if __name__ == "__main__": + try: + web.run_app(APP, host="localhost", port=CONFIG.PORT) + except Exception as error: + raise error diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/__init__.py new file mode 100644 index 000000000..4ef47b33a --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/__init__.py @@ -0,0 +1,5 @@ +from .allow_callers_claims_validation import AllowedCallersClaimsValidator + +__all__ = [ + "AllowedCallersClaimsValidator" +] \ No newline at end of file diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/allow_callers_claims_validation.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/allow_callers_claims_validation.py new file mode 100644 index 000000000..9da84cd1e --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/allow_callers_claims_validation.py @@ -0,0 +1,39 @@ +from typing import Awaitable, Callable, Dict, List + +from botframework.connector.auth import JwtTokenValidation, SkillValidation + +from ..config import DefaultConfig + + +class AllowedCallersClaimsValidator: + + config_key = "ALLOWED_CALLERS" + + def __init__(self, config: DefaultConfig): + if not config: + raise TypeError( + "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 + self._allowed_callers = getattr(config) + + @property + def claims_validator(self) -> Callable[[List[Dict]], Awaitable]: + async def allow_callers_claims_validator(claims: Dict[str, object]): + # if _allowedCallers is None we allow all calls + if 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. + app_id = JwtTokenValidation.get_app_id_from_claims(claims) + if app_id not in self._allowed_callers: + raise PermissionError( + f"Received a request from a bot with an app ID of \"{app_id}\"." + f" To enable requests from this caller, add the app ID to your configuration file." + ) + + return + + return allow_callers_claims_validator diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/__init__.py new file mode 100644 index 000000000..f95fbbbad --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .echo_bot import EchoBot + +__all__ = ["EchoBot"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/echo_bot.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/echo_bot.py new file mode 100644 index 000000000..90a094640 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/echo_bot.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core import ActivityHandler, MessageFactory, TurnContext +from botbuilder.schema import ChannelAccount + + +class EchoBot(ActivityHandler): + async def on_members_added_activity( + self, members_added: [ChannelAccount], turn_context: TurnContext + ): + for member in members_added: + if member.id != turn_context.activity.recipient.id: + await turn_context.send_activity("Hello and welcome!") + + async def on_message_activity(self, turn_context: TurnContext): + return await turn_context.send_activity( + MessageFactory.text(f"Echo: {turn_context.activity.text}") + ) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py new file mode 100644 index 000000000..e007d0fa9 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + +""" Bot Configuration """ + + +class DefaultConfig: + """ Bot Configuration """ + + PORT = 3978 + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-new-rg.json b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 000000000..b68e51ec6 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-new-rg.json @@ -0,0 +1,264 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "newAppServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan. Defaults to \"westus\"." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "appServicePlanName": "[parameters('newAppServicePlanName')]", + "resourcesLocation": "[parameters('newAppServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]" + }, + "resources": [ + { + "name": "[parameters('groupName')]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('groupLocation')]", + "properties": { + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('groupName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "name": "[variables('appServicePlanName')]", + "perSiteScaling": false, + "reserved": true, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app,linux", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[variables('appServicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", + "value": "true" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2016-08-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "phpVersion": "", + "pythonVersion": "", + "nodeVersion": "", + "linuxFxVersion": "PYTHON|3.7", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "remoteDebuggingVersion": "VS2017", + "httpLoggingEnabled": true, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": false, + "appCommandLine": "gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:APP", + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": false, + "virtualDirectories": null + } + ], + "winAuthAdminState": 0, + "winAuthTenantState": 0, + "customAppPoolIdentityAdminState": false, + "customAppPoolIdentityTenantState": false, + "loadBalancing": "LeastRequests", + "routingRules": [], + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "vnetName": "", + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ], + "outputs": {} + } + } + } + ] +} \ No newline at end of file diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-preexisting-rg.json b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..66b044b52 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,242 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "F0", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2016-09-01", + "name": "[variables('servicePlanName')]", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "name": "[variables('servicePlanName')]", + "perSiteScaling": false, + "reserved": true, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2016-08-01", + "name": "[variables('webAppName')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" + ], + "kind": "app,linux", + "properties": { + "enabled": true, + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "reserved": true, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": false, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "siteConfig": { + "appSettings": [ + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + }, + { + "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", + "value": "true" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2016-08-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "phpVersion": "", + "pythonVersion": "", + "nodeVersion": "", + "linuxFxVersion": "PYTHON|3.7", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "remoteDebuggingVersion": "VS2017", + "httpLoggingEnabled": true, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": false, + "appCommandLine": "gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:APP", + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": false, + "virtualDirectories": null + } + ], + "winAuthAdminState": 0, + "winAuthTenantState": 0, + "customAppPoolIdentityAdminState": false, + "customAppPoolIdentityTenantState": false, + "loadBalancing": "LeastRequests", + "routingRules": [], + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "vnetName": "", + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} \ No newline at end of file diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/requirements.txt b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/requirements.txt new file mode 100644 index 000000000..52eb5fe1e --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/requirements.txt @@ -0,0 +1,2 @@ +aiohttp +botbuilder-core>=4.7.0 diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/skill_adapter_with_error_handler.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/skill_adapter_with_error_handler.py new file mode 100644 index 000000000..0decd3d53 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/skill_adapter_with_error_handler.py @@ -0,0 +1,86 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import sys +import traceback +from datetime import datetime + +from botbuilder.core import ( + BotFrameworkAdapter, + BotFrameworkAdapterSettings, + ConversationState, + MessageFactory, + TurnContext, +) +from botbuilder.schema import ActivityTypes, Activity, InputHints + + +class SkillAdapterWithErrorHandler(BotFrameworkAdapter): + def __init__( + self, + settings: BotFrameworkAdapterSettings, + conversation_state: ConversationState = None, + ): + super().__init__(settings) + self._conversation_state = conversation_state + + # Catch-all for errors. + async def on_error(context: TurnContext, error: Exception): + # This check writes out errors to console log + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() + + # Send a message to the user + error_message_text = "The skill encountered an error or bug." + error_message = MessageFactory.text(error_message_text, error_message_text, InputHints.ignoring_input) + await context.send_activity(error_message) + + error_message_text = "To continue to run this bot, please fix the bot source code." + error_message = MessageFactory.text(error_message_text, error_message_text, InputHints.ignoring_input) + await context.send_activity(error_message) + + # Send a trace activity if we're talking to the Bot Framework Emulator + if context.activity.channel_id == "emulator": + # Create a trace activity that contains the error object + trace_activity = Activity( + label="TurnError", + name="on_turn_error Trace", + timestamp=datetime.utcnow(), + type=ActivityTypes.trace, + value=f"{error}", + value_type="https://www.botframework.com/schemas/error", + ) + # Send a trace activity, which will be displayed in Bot Framework Emulator + await context.send_activity(trace_activity) + + # Clear out state + nonlocal self + if self._conversation_state: + try: + await self._conversation_state.delete(context) + except Exception as exception: + print( + f"\n Exception caught on attempting to Delete ConversationState : {exception}", + file=sys.stderr + ) + traceback.print_exc() + + # Send and EndOfConversation activity to the skill caller with the error to end the conversation + # and let the caller decide what to do. + end_of_conversation = Activity(type=ActivityTypes.end_of_conversation) + end_of_conversation.code = "SkillError" + end_of_conversation.text = str(error) + await context.send_activity(end_of_conversation) + + # Send a trace activity, which will be displayed in the Bot Framework Emulator + # Note: we return the entire exception in the value property to help the developer, + # this should not be done in prod. + await context.send_trace_activity( + "OnTurnError Trace", + str(error), + "https://www.botframework.com/schemas/error", + "TurnError" + ) + + self.on_turn_error = on_error From 6349deb8a36ff531344347ff2a11a81510e91008 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Tue, 18 Feb 2020 18:08:43 -0800 Subject: [PATCH 04/38] pylint: Initial echo skill with dialog --- .../botbuilder/dialogs/skills/skill_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py index 79a451dd7..710f79e4e 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -113,7 +113,7 @@ async def _send_to_skill( ) # Inspect the skill response status - if not (200 <= response.status <= 299): + if not 200 <= response.status <= 299: raise Exception( f'Error invoking the skill id: "{skill_info.id}" at "{skill_info.skill_endpoint}"' f" (status is {response.status}). \r\n {response.body}" From f9b80dd7a01774d1cc488bcbdc1afd5757298e78 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 21 Feb 2020 17:34:26 -0800 Subject: [PATCH 05/38] made integration package for aiohttp, dialog root bot for testing --- .../botbuilder/core/__init__.py | 2 - .../botbuilder/dialogs/__init__.py | 1 + libraries/botbuilder-dialogs/setup.py | 1 + .../botbuilder-integration-aiohttp/README.rst | 83 +++++++ .../integration/aiohttp/__init__.py | 16 ++ .../botbuilder/integration/aiohttp/about.py | 14 ++ .../aiohttp/aiohttp_channel_service.py | 176 +++++++++++++ ...tp_channel_service_exception_middleware.py | 29 +++ .../aiohttp}/bot_framework_http_client.py | 4 +- .../integration/aiohttp/skills/__init__.py | 4 + .../aiohttp/skills/skill_http_client.py | 42 ++++ .../requirements.txt | 4 + .../botbuilder-integration-aiohttp/setup.cfg | 2 + .../botbuilder-integration-aiohttp/setup.py | 54 ++++ .../dialog-echo-skill-bot/app.py | 17 +- .../adapter_with_error_handler.py | 56 +++++ .../dialog-to-dialog/dialog-root-bot/app.py | 81 ++++++ .../authentication/__init__.py | 3 + .../allow_callers_claims_validation.py | 39 +++ .../dialog-root-bot/bots/__init__.py | 4 + .../dialog-root-bot/bots/root_bot.py | 63 +++++ .../dialog-root-bot/cards/welcomeCard.json | 46 ++++ .../dialog-root-bot/config.py | 32 +++ .../dialogs/booking_details.py | 18 ++ .../dialog-root-bot/dialogs/location.py | 11 + .../dialog-root-bot/dialogs/main_dialog.py | 232 ++++++++++++++++++ .../dialog-root-bot/helpers/__init__.py | 6 + .../helpers/activity_helper.py | 37 +++ .../dialog-root-bot/helpers/dialog_helper.py | 19 ++ .../dialog-root-bot/helpers/luis_helper.py | 102 ++++++++ .../dialog-root-bot/middleware/__init__.py | 4 + .../middleware/dummy_middleware.py | 32 +++ 32 files changed, 1223 insertions(+), 11 deletions(-) create mode 100644 libraries/botbuilder-integration-aiohttp/README.rst create mode 100644 libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/__init__.py create mode 100644 libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/about.py create mode 100644 libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service.py create mode 100644 libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service_exception_middleware.py rename libraries/{botbuilder-core/botbuilder/core => botbuilder-integration-aiohttp/botbuilder/integration/aiohttp}/bot_framework_http_client.py (97%) create mode 100644 libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/__init__.py create mode 100644 libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py create mode 100644 libraries/botbuilder-integration-aiohttp/requirements.txt create mode 100644 libraries/botbuilder-integration-aiohttp/setup.cfg create mode 100644 libraries/botbuilder-integration-aiohttp/setup.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/__init__.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allow_callers_claims_validation.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/__init__.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/cards/welcomeCard.json create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/booking_details.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/location.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/__init__.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/activity_helper.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/dialog_helper.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/luis_helper.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/__init__.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/dummy_middleware.py diff --git a/libraries/botbuilder-core/botbuilder/core/__init__.py b/libraries/botbuilder-core/botbuilder/core/__init__.py index cdac7c42c..041602a83 100644 --- a/libraries/botbuilder-core/botbuilder/core/__init__.py +++ b/libraries/botbuilder-core/botbuilder/core/__init__.py @@ -21,7 +21,6 @@ from .conversation_state import ConversationState from .intent_score import IntentScore from .invoke_response import InvokeResponse -from .bot_framework_http_client import BotFrameworkHttpClient from .memory_storage import MemoryStorage from .memory_transcript_store import MemoryTranscriptStore from .message_factory import MessageFactory @@ -61,7 +60,6 @@ "conversation_reference_extension", "IntentScore", "InvokeResponse", - "BotFrameworkHttpClient", "MemoryStorage", "MemoryTranscriptStore", "MessageFactory", diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py index 2d0447c3e..d13238cca 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py @@ -19,6 +19,7 @@ from .waterfall_step_context import WaterfallStepContext from .prompts import * from .choices import * +from .skills import * __all__ = [ "ComponentDialog", diff --git a/libraries/botbuilder-dialogs/setup.py b/libraries/botbuilder-dialogs/setup.py index ae24e3833..f242baec4 100644 --- a/libraries/botbuilder-dialogs/setup.py +++ b/libraries/botbuilder-dialogs/setup.py @@ -43,6 +43,7 @@ "botbuilder.dialogs", "botbuilder.dialogs.prompts", "botbuilder.dialogs.choices", + "botbuilder.dialogs.skills", ], install_requires=REQUIRES + TEST_REQUIRES, tests_require=TEST_REQUIRES, diff --git a/libraries/botbuilder-integration-aiohttp/README.rst b/libraries/botbuilder-integration-aiohttp/README.rst new file mode 100644 index 000000000..f92429436 --- /dev/null +++ b/libraries/botbuilder-integration-aiohttp/README.rst @@ -0,0 +1,83 @@ + +========================================= +BotBuilder-Integration-Aiohttp for Python +========================================= + +.. image:: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI?branchName=master + :target: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI + :align: right + :alt: Azure DevOps status for master branch +.. image:: https://badge.fury.io/py/botbuilder-core.svg + :target: https://badge.fury.io/py/botbuilder-core + :alt: Latest PyPI package version + +Within the Bot Framework, This library enables you to integrate your bot within an aiohttp web application. + +How to Install +============== + +.. code-block:: python + + pip install botbuilder-integration-aiohttp + + +Documentation/Wiki +================== + +You can find more information on the botbuilder-python project by visiting our `Wiki`_. + +Requirements +============ + +* `Python >= 3.7.0`_ + + +Source Code +=========== +The latest developer version is available in a github repository: +https://github.com/Microsoft/botbuilder-python/ + + +Contributing +============ + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the `Microsoft Open Source Code of Conduct`_. +For more information see the `Code of Conduct FAQ`_ or +contact `opencode@microsoft.com`_ with any additional questions or comments. + +Reporting Security Issues +========================= + +Security issues and bugs should be reported privately, via email, to the Microsoft Security +Response Center (MSRC) at `secure@microsoft.com`_. You should +receive a response within 24 hours. If for some reason you do not, please follow up via +email to ensure we received your original message. Further information, including the +`MSRC PGP`_ key, can be found in +the `Security TechCenter`_. + +License +======= + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the MIT_ License. + +.. _Wiki: https://github.com/Microsoft/botbuilder-python/wiki +.. _Python >= 3.7.0: https://www.python.org/downloads/ +.. _MIT: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt +.. _Microsoft Open Source Code of Conduct: https://opensource.microsoft.com/codeofconduct/ +.. _Code of Conduct FAQ: https://opensource.microsoft.com/codeofconduct/faq/ +.. _opencode@microsoft.com: mailto:opencode@microsoft.com +.. _secure@microsoft.com: mailto:secure@microsoft.com +.. _MSRC PGP: https://technet.microsoft.com/en-us/security/dn606155 +.. _Security TechCenter: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt + +.. `_ \ No newline at end of file diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/__init__.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/__init__.py new file mode 100644 index 000000000..1bb31e665 --- /dev/null +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/__init__.py @@ -0,0 +1,16 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +from .aiohttp_channel_service import aiohttp_channel_service_routes +from .aiohttp_channel_service_exception_middleware import aiohttp_error_middleware +from .bot_framework_http_client import BotFrameworkHttpClient + +__all__ = [ + "aiohttp_channel_service_routes", + "aiohttp_error_middleware", + "BotFrameworkHttpClient", +] diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/about.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/about.py new file mode 100644 index 000000000..c0bfc2c92 --- /dev/null +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/about.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + +__title__ = "botbuilder-integration-aiohttp" +__version__ = ( + os.environ["packageVersion"] if "packageVersion" in os.environ else "4.7.1" +) +__uri__ = "https://www.github.com/Microsoft/botbuilder-python" +__author__ = "Microsoft" +__description__ = "Microsoft Bot Framework Bot Builder" +__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python." +__license__ = "MIT" diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service.py new file mode 100644 index 000000000..af2545d89 --- /dev/null +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service.py @@ -0,0 +1,176 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import json +from typing import List, Union, Type + +from aiohttp.web import RouteTableDef, Request, Response +from msrest.serialization import Model + +from botbuilder.schema import ( + Activity, + AttachmentData, + ConversationParameters, + Transcript, +) + +from botbuilder.core import ChannelServiceHandler + + +async def deserialize_from_body( + request: Request, target_model: Type[Model] +) -> Activity: + if "application/json" in request.headers["Content-Type"]: + body = await request.json() + else: + return Response(status=415) + + return target_model().deserialize(body) + + +def get_serialized_response(model_or_list: Union[Model, List[Model]]) -> Response: + if isinstance(model_or_list, Model): + json_obj = model_or_list.serialize() + else: + json_obj = [model.serialize() for model in model_or_list] + + return Response(body=json.dumps(json_obj), content_type="application/json") + + +def aiohttp_channel_service_routes( + handler: ChannelServiceHandler, base_url: str = "" +) -> RouteTableDef: + # pylint: disable=unused-variable + routes = RouteTableDef() + + @routes.post(base_url + "/v3/conversations/{conversation_id}/activities") + async def send_to_conversation(request: Request): + activity = await deserialize_from_body(request, Activity) + result = await handler.handle_send_to_conversation( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + activity, + ) + + return get_serialized_response(result) + + @routes.post( + base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}" + ) + async def reply_to_activity(request: Request): + activity = await deserialize_from_body(request, Activity) + result = await handler.handle_reply_to_activity( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + request.match_info["activity_id"], + activity, + ) + + return get_serialized_response(result) + + @routes.put( + base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}" + ) + async def update_activity(request: Request): + activity = await deserialize_from_body(request, Activity) + result = await handler.handle_update_activity( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + request.match_info["activity_id"], + activity, + ) + + return get_serialized_response(result) + + @routes.delete( + base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}" + ) + async def delete_activity(request: Request): + await handler.handle_delete_activity( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + request.match_info["activity_id"], + ) + + return Response() + + @routes.get( + base_url + + "/v3/conversations/{conversation_id}/activities/{activity_id}/members" + ) + async def get_activity_members(request: Request): + result = await handler.handle_get_activity_members( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + request.match_info["activity_id"], + ) + + return get_serialized_response(result) + + @routes.post(base_url + "/") + async def create_conversation(request: Request): + conversation_parameters = deserialize_from_body(request, ConversationParameters) + result = await handler.handle_create_conversation( + request.headers.get("Authorization"), conversation_parameters + ) + + return get_serialized_response(result) + + @routes.get(base_url + "/") + async def get_conversation(request: Request): + # TODO: continuation token? + result = await handler.handle_get_conversations( + request.headers.get("Authorization") + ) + + return get_serialized_response(result) + + @routes.get(base_url + "/v3/conversations/{conversation_id}/members") + async def get_conversation_members(request: Request): + result = await handler.handle_get_conversation_members( + request.headers.get("Authorization"), request.match_info["conversation_id"], + ) + + return get_serialized_response(result) + + @routes.get(base_url + "/v3/conversations/{conversation_id}/pagedmembers") + async def get_conversation_paged_members(request: Request): + # TODO: continuation token? page size? + result = await handler.handle_get_conversation_paged_members( + request.headers.get("Authorization"), request.match_info["conversation_id"], + ) + + return get_serialized_response(result) + + @routes.delete(base_url + "/v3/conversations/{conversation_id}/members/{member_id}") + async def delete_conversation_member(request: Request): + result = await handler.handle_delete_conversation_member( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + request.match_info["member_id"], + ) + + return get_serialized_response(result) + + @routes.post(base_url + "/v3/conversations/{conversation_id}/activities/history") + async def send_conversation_history(request: Request): + transcript = deserialize_from_body(request, Transcript) + result = await handler.handle_send_conversation_history( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + transcript, + ) + + return get_serialized_response(result) + + @routes.post(base_url + "/v3/conversations/{conversation_id}/attachments") + async def upload_attachment(request: Request): + attachment_data = deserialize_from_body(request, AttachmentData) + result = await handler.handle_upload_attachment( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + attachment_data, + ) + + return get_serialized_response(result) + + return routes diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service_exception_middleware.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service_exception_middleware.py new file mode 100644 index 000000000..7c5091121 --- /dev/null +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service_exception_middleware.py @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from aiohttp.web import ( + middleware, + HTTPNotImplemented, + HTTPUnauthorized, + HTTPNotFound, + HTTPInternalServerError, +) + +from botbuilder.core import BotActionNotImplementedError + + +@middleware +async def aiohttp_error_middleware(request, handler): + try: + response = await handler(request) + return response + except BotActionNotImplementedError: + raise HTTPNotImplemented() + except NotImplementedError: + raise HTTPNotImplemented() + except PermissionError: + raise HTTPUnauthorized() + except KeyError: + raise HTTPNotFound() + except Exception: + raise HTTPInternalServerError() diff --git a/libraries/botbuilder-core/botbuilder/core/bot_framework_http_client.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py similarity index 97% rename from libraries/botbuilder-core/botbuilder/core/bot_framework_http_client.py rename to libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py index 6ee6a87f3..74a1fdfce 100644 --- a/libraries/botbuilder-core/botbuilder/core/bot_framework_http_client.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py @@ -14,8 +14,8 @@ MicrosoftAppCredentials, ) -from . import InvokeResponse -from .skills.bot_framework_client import BotFrameworkClient +from botbuilder.core import InvokeResponse +from botbuilder.core.skills import BotFrameworkClient class BotFrameworkHttpClient(BotFrameworkClient): diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/__init__.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/__init__.py new file mode 100644 index 000000000..71aaa71cf --- /dev/null +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/__init__.py @@ -0,0 +1,4 @@ +from .skill_http_client import SkillHttpClient + + +__all__ = ["SkillHttpClient"] diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py new file mode 100644 index 000000000..b27a4058b --- /dev/null +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py @@ -0,0 +1,42 @@ +from logging import Logger + +from botbuilder.schema import Activity +from botframework.connector.auth import ( + ChannelProvider, + CredentialProvider, +) +from botbuilder.core import InvokeResponse, TurnContext +from botbuilder.core.skills import BotFrameworkSkill, SkillConversationIdFactory +from botbuilder.integration.aiohttp import BotFrameworkHttpClient + + +class SkillHttpClient(BotFrameworkHttpClient): + def __init__( + self, + credential_provider: CredentialProvider, + conversation_id_factory: SkillConversationIdFactory, + channel_provider: ChannelProvider = None, + logger: Logger = None, + ): + super().__init__(credential_provider, channel_provider, logger) + self._conversation_id_factory = conversation_id_factory + + async def post_activity( + self, + from_bot_id: str, + to_skill: BotFrameworkSkill, + callback_url: str, + activity: Activity, + ) -> InvokeResponse: + skill_conversation_id = await self._conversation_id_factory.create_skill_conversation_id( + TurnContext.get_conversation_reference(activity) + ) + + return await super().post_activity( + from_bot_id, + to_skill.app_id, + to_skill.skill_endpoint, + callback_url, + skill_conversation_id, + activity, + ) diff --git a/libraries/botbuilder-integration-aiohttp/requirements.txt b/libraries/botbuilder-integration-aiohttp/requirements.txt new file mode 100644 index 000000000..1d6f7ab31 --- /dev/null +++ b/libraries/botbuilder-integration-aiohttp/requirements.txt @@ -0,0 +1,4 @@ +msrest==0.6.10 +botframework-connector>=4.7.1 +botbuilder-schema>=4.7.1 +aiohttp>=3.6.2 \ No newline at end of file diff --git a/libraries/botbuilder-integration-aiohttp/setup.cfg b/libraries/botbuilder-integration-aiohttp/setup.cfg new file mode 100644 index 000000000..68c61a226 --- /dev/null +++ b/libraries/botbuilder-integration-aiohttp/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=0 \ No newline at end of file diff --git a/libraries/botbuilder-integration-aiohttp/setup.py b/libraries/botbuilder-integration-aiohttp/setup.py new file mode 100644 index 000000000..df1778810 --- /dev/null +++ b/libraries/botbuilder-integration-aiohttp/setup.py @@ -0,0 +1,54 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +from setuptools import setup + +VERSION = os.environ["packageVersion"] if "packageVersion" in os.environ else "4.7.1" +REQUIRES = [ + "botbuilder-schema>=4.7.1", + "botframework-connector>=4.7.1", + "botbuilder-core>=4.7.1", + "aiohttp==3.6.2", +] + +root = os.path.abspath(os.path.dirname(__file__)) + +with open(os.path.join(root, "botbuilder", "integration", "aiohttp", "about.py")) as f: + package_info = {} + info = f.read() + exec(info, package_info) + +with open(os.path.join(root, "README.rst"), encoding="utf-8") as f: + long_description = f.read() + +setup( + name=package_info["__title__"], + version=package_info["__version__"], + url=package_info["__uri__"], + author=package_info["__author__"], + description=package_info["__description__"], + keywords=[ + "BotBuilderIntegrationAiohttp", + "bots", + "ai", + "botframework", + "botbuilder", + ], + long_description=long_description, + long_description_content_type="text/x-rst", + license=package_info["__license__"], + packages=[ + "botbuilder.integration.aiohttp", + "botbuilder.integration.aiohttp.skills", + ], + install_requires=REQUIRES, + classifiers=[ + "Programming Language :: Python :: 3.7", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Development Status :: 5 - Production/Stable", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + ], +) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py index 4d4ee3c51..c2e87e326 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py @@ -1,18 +1,16 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import sys -import traceback -from datetime import datetime - from aiohttp import web from aiohttp.web import Request, Response, json_response from botbuilder.core import ( BotFrameworkAdapterSettings, - TurnContext + ConversationState, + MemoryStorage, + UserState ) from botbuilder.core.integration import aiohttp_error_middleware -from botbuilder.schema import Activity, ActivityTypes +from botbuilder.schema import Activity from botframework.connector.auth import AuthenticationConfiguration from authentication import AllowedCallersClaimsValidator @@ -22,6 +20,11 @@ CONFIG = DefaultConfig() +# Create MemoryStorage, UserState and ConversationState +MEMORY = MemoryStorage() +USER_STATE = UserState(MEMORY) +CONVERSATION_STATE = ConversationState(MEMORY) + # Create adapter. # See https://aka.ms/about-bot-adapter to learn more about how bots work. VALIDATOR = AllowedCallersClaimsValidator(CONFIG).claims_validator @@ -30,7 +33,7 @@ CONFIG.APP_PASSWORD, auth_configuration=AuthenticationConfiguration(claims_validator=VALIDATOR) ) -ADAPTER = SkillAdapterWithErrorHandler(SETTINGS) +ADAPTER = SkillAdapterWithErrorHandler(SETTINGS, CONVERSATION_STATE) # Create the Bot BOT = EchoBot() diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py new file mode 100644 index 000000000..4e24fcb79 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py @@ -0,0 +1,56 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import sys +import traceback +from datetime import datetime + +from botbuilder.core import ( + BotFrameworkAdapter, + BotFrameworkAdapterSettings, + ConversationState, + TurnContext, +) +from botbuilder.schema import ActivityTypes, Activity + + +class AdapterWithErrorHandler(BotFrameworkAdapter): + def __init__( + self, + settings: BotFrameworkAdapterSettings, + conversation_state: ConversationState, + ): + super().__init__(settings) + self._conversation_state = conversation_state + + # Catch-all for errors. + async def on_error(context: TurnContext, error: Exception): + # This check writes out errors to console log + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") + await context.send_activity( + "To continue to run this bot, please fix the bot source code." + ) + # Send a trace activity if we're talking to the Bot Framework Emulator + if context.activity.channel_id == "emulator": + # Create a trace activity that contains the error object + trace_activity = Activity( + label="TurnError", + name="on_turn_error Trace", + timestamp=datetime.utcnow(), + type=ActivityTypes.trace, + value=f"{error}", + value_type="https://www.botframework.com/schemas/error", + ) + # Send a trace activity, which will be displayed in Bot Framework Emulator + await context.send_activity(trace_activity) + + # Clear out state + nonlocal self + await self._conversation_state.delete(context) + + self.on_turn_error = on_error diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py new file mode 100644 index 000000000..eb99b1c11 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py @@ -0,0 +1,81 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from aiohttp import web +from aiohttp.web import Request, Response +from botbuilder.core import ( + BotFrameworkAdapterSettings, + BotFrameworkHttpClient, + ConversationState, + MemoryStorage, + TurnContext, + UserState, + BotFrameworkAdapter, +) +from botbuilder.core.integration import ( + aiohttp_channel_service_routes, + aiohttp_error_middleware, +) +from botbuilder.core.skills import SkillConversationIdFactory, SkillHandler +from botbuilder.schema import Activity, ActivityTypes +from botframework.connector.auth import ( + AuthenticationConfiguration, + SimpleCredentialProvider, +) + +from bots import RootBot +from config import DefaultConfig, SkillConfiguration +from adapter_with_error_handler import AdapterWithErrorHandler + +CONFIG = DefaultConfig() +SKILL_CONFIG = SkillConfiguration() + +CREDENTIAL_PROVIDER = SimpleCredentialProvider(CONFIG.APP_ID, CONFIG.APP_PASSWORD) +CLIENT = BotFrameworkHttpClient(CREDENTIAL_PROVIDER) + +# Create MemoryStorage, UserState and ConversationState +MEMORY = MemoryStorage() +USER_STATE = UserState(MEMORY) +CONVERSATION_STATE = ConversationState(MEMORY) +ID_FACTORY = SkillConversationIdFactory(MEMORY) + +# Create adapter. +# See https://aka.ms/about-bot-adapter to learn more about how bots work. +SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD) +ADAPTER = AdapterWithErrorHandler(SETTINGS, CONVERSATION_STATE) + +# Create the Bot +BOT = RootBot(CONVERSATION_STATE, SKILL_CONFIG, ID_FACTORY, CLIENT, CONFIG) + +SKILL_HANDLER = SkillHandler( + ADAPTER, BOT, ID_FACTORY, CREDENTIAL_PROVIDER, AuthenticationConfiguration() +) + + +# Listen for incoming requests on /api/messages +async def messages(req: Request) -> Response: + # Main bot message handler. + if "application/json" in req.headers["Content-Type"]: + body = await req.json() + else: + return Response(status=415) + + activity = Activity().deserialize(body) + auth_header = req.headers["Authorization"] if "Authorization" in req.headers else "" + + try: + await ADAPTER.process_activity(activity, auth_header, BOT.on_turn) + return Response(status=201) + except Exception as exception: + raise exception + + +APP = web.Application(middlewares=[aiohttp_error_middleware]) +APP.router.add_post("/api/messages", messages) +APP.router.add_routes(aiohttp_channel_service_routes(SKILL_HANDLER, "/api/skills")) + +if __name__ == "__main__": + try: + web.run_app(APP, host="localhost", port=CONFIG.PORT) + except Exception as error: + raise error diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/__init__.py new file mode 100644 index 000000000..ebbe2ac15 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/__init__.py @@ -0,0 +1,3 @@ +from .allow_callers_claims_validation import AllowedCallersClaimsValidator + +__all__ = ["AllowedCallersClaimsValidator"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allow_callers_claims_validation.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allow_callers_claims_validation.py new file mode 100644 index 000000000..2d5b30060 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allow_callers_claims_validation.py @@ -0,0 +1,39 @@ +from typing import Awaitable, Callable, Dict, List + +from botframework.connector.auth import JwtTokenValidation, SkillValidation + +from ..config import DefaultConfig + + +class AllowedCallersClaimsValidator: + + config_key = "ALLOWED_CALLERS" + + def __init__(self, config: DefaultConfig): + if not config: + raise TypeError( + "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 + self._allowed_callers = getattr(config) + + @property + def claims_validator(self) -> Callable[[List[Dict]], Awaitable]: + async def allow_callers_claims_validator(claims: Dict[str, object]): + # if _allowedCallers is None we allow all calls + if 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. + app_id = JwtTokenValidation.get_app_id_from_claims(claims) + if app_id not in self._allowed_callers: + raise PermissionError( + f'Received a request from a bot with an app ID of "{app_id}".' + f" To enable requests from this caller, add the app ID to your configuration file." + ) + + return + + return allow_callers_claims_validator diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/__init__.py new file mode 100644 index 000000000..be7e157a7 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/__init__.py @@ -0,0 +1,4 @@ +from .root_bot import RootBot + + +__all__ = ["RootBot"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py new file mode 100644 index 000000000..0561714ba --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py @@ -0,0 +1,63 @@ +import json +import os.path + +from typing import List + +from botbuilder.core import ( + ActivityHandler, + BotFrameworkHttpClient, + ConversationState, + MessageFactory, + TurnContext, +) +from botbuilder.core.skills import SkillConversationIdFactory +from botbuilder.dialogs import Dialog + +from botbuilder.schema import ActivityTypes, Attachment, ChannelAccount + +from config import DefaultConfig, SkillConfiguration +from helpers.dialog_helper import DialogHelper + + +class RootBot(ActivityHandler): + def __init__( + self, conversation_state: ConversationState, main_dialog: Dialog, + ): + self._conversation_state = conversation_state + self._main_dialog = main_dialog + + 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 + await DialogHelper.run_dialog( + self._main_dialog, + turn_context, + self._conversation_state.create_property("DialogState"), + ) + else: + await super().on_turn(turn_context) + + async def on_members_added_activity( + self, members_added: List[ChannelAccount], turn_context: TurnContext + ): + for member in members_added: + if member.id != turn_context.activity.recipient.id: + welcome_card = self._create_adaptive_card_attachment() + activity = MessageFactory.attachment(welcome_card) + await turn_context.send_activity(activity) + await DialogHelper.run_dialog( + self._main_dialog, + turn_context, + self._conversation_state.create_property("DialogState"), + ) + + def _create_adaptive_card_attachment(self) -> Attachment: + relative_path = os.path.abspath(os.path.dirname(__file__)) + path = os.path.join(relative_path, "../cards/welcomeCard.json") + with open(path) as in_file: + card = json.load(in_file) + + return Attachment( + content_type="application/vnd.microsoft.card.adaptive", content=card + ) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/cards/welcomeCard.json b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/cards/welcomeCard.json new file mode 100644 index 000000000..cc10cda9f --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/cards/welcomeCard.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [ + { + "type": "Image", + "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU", + "size": "stretch" + }, + { + "type": "TextBlock", + "spacing": "medium", + "size": "default", + "weight": "bolder", + "text": "Welcome to Bot Framework!", + "wrap": true, + "maxLines": 0 + }, + { + "type": "TextBlock", + "size": "default", + "isSubtle": "true", + "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.", + "wrap": true, + "maxLines": 0 + } + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "Get an overview", + "url": "https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0" + }, + { + "type": "Action.OpenUrl", + "title": "Ask a question", + "url": "https://stackoverflow.com/questions/tagged/botframework" + }, + { + "type": "Action.OpenUrl", + "title": "Learn how to deploy", + "url": "https://docs.microsoft.com/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0" + } + ] +} \ No newline at end of file diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py new file mode 100644 index 000000000..f2a9e1f6e --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +from typing import Dict +from botbuilder.core.skills import BotFrameworkSkill + +""" Bot Configuration """ + + +class DefaultConfig: + """ Bot Configuration """ + + PORT = 3428 + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") + SKILL_HOST_ENDPOINT = "http://localhost:3428/api/skills" + SKILLS = [ + { + "id": "SkillBot", + "app_id": "", + "skill_endpoint": "http://localhost:3978/api/messages", + }, + ] + + +class SkillConfiguration: + SKILL_HOST_ENDPOINT = DefaultConfig.SKILL_HOST_ENDPOINT + SKILLS: Dict[str, BotFrameworkSkill] = { + skill["id"]: BotFrameworkSkill(**skill) for skill in DefaultConfig.SKILLS + } diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/booking_details.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/booking_details.py new file mode 100644 index 000000000..db7c4c60c --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/booking_details.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class BookingDetails: + def __init__( + self, + destination: str = None, + origin: str = None, + travel_date: str = None, + unsupported_airports=None, + ): + if unsupported_airports is None: + unsupported_airports = [] + self.destination = destination + self.origin = origin + self.travel_date = travel_date + self.unsupported_airports = unsupported_airports diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/location.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/location.py new file mode 100644 index 000000000..045aaedfa --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/location.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class Location: + def __init__( + self, latitude: str = None, longitude: str = None, postal_code: str = None, + ): + self.latitude = latitude + self.longitude = longitude + self.postal_code = postal_code diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py new file mode 100644 index 000000000..6877aa141 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py @@ -0,0 +1,232 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import List + +from jsonpickle import encode +from botbuilder.dialogs import ( + ComponentDialog, + WaterfallDialog, + WaterfallStepContext, + DialogTurnResult, +) +from botbuilder.dialogs.choices import Choice, ListStyle +from botbuilder.dialogs.prompts import PromptOptions, ChoicePrompt +from botbuilder.dialogs.skills import SkillDialogOptions, SkillDialog, SkillDialogArgs +from botbuilder.core import ConversationState, MessageFactory +from botbuilder.core.skills import BotFrameworkSkill, ConversationIdFactoryBase +from botbuilder.schema import Activity, ActivityTypes, InputHints +from botbuilder.integration.aiohttp.skills import SkillHttpClient + +from .booking_details import BookingDetails +from ..config import DefaultConfig, SkillConfiguration + + +class MainDialog(ComponentDialog): + def __init__( + self, + conversation_state: ConversationState, + conversation_id_factory: ConversationIdFactoryBase, + skill_client: SkillHttpClient, + skills_config: SkillConfiguration, + configuration: DefaultConfig, + ): + super(MainDialog, self).__init__(MainDialog.__name__) + + self._selected_skill_key = ( + f"{MainDialog.__module__}.{MainDialog.__name__}.SelectedSkillKey" + ) + + bot_id = configuration.APP_ID + if not bot_id: + raise TypeError("App Id is not in configuration") + + self._skills_config = skills_config + if not self._skills_config: + raise TypeError("Skills configuration cannot be None") + + if not skill_client: + raise TypeError("skill_client cannot be None") + + if not conversation_state: + raise TypeError("conversation_state cannot be None") + + # ChoicePrompt to render available skills and skill actions + self.add_dialog(ChoicePrompt(ChoicePrompt.__name__)) + + # SkillDialog used to wrap interaction with the selected skill + skill_dialog_options = SkillDialogOptions( + bot_id=bot_id, + conversation_id_factory=conversation_id_factory, + skill_client=skill_client, + skill_host_endpoint=skills_config.SKILL_HOST_ENDPOINT, + ) + + self.add_dialog(SkillDialog(skill_dialog_options, conversation_state)) + + # Main waterfall dialog for this bot + self.add_dialog( + WaterfallDialog( + WaterfallDialog.__name__, + [self.intro_step, self.act_step, self.final_step], + ) + ) + + self.initial_dialog_id = WaterfallDialog.__name__ + + async def select_skill_step( + self, step_context: WaterfallStepContext + ) -> DialogTurnResult: + # Create the PromptOptions from the skill configuration which contain the list of configured skills. + options = PromptOptions( + prompt=MessageFactory.text("What skill would you like to call?"), + retry_prompt=MessageFactory.text( + "That was not a valid choice, please select a valid skill." + ), + choices=[Choice(value=skill.id) for skill in self._skills_config.SKILLS], + ) + + # Prompt the user to select a skill. + return await step_context.prompt(ChoicePrompt.__name__, options) + + async def select_skill_action_step( + self, step_context: WaterfallStepContext + ) -> DialogTurnResult: + # Get the skill info based on the selected skill. + selected_skill_id = step_context.result + selected_skill = next( + ( + skill + for skill in self._skills_config.SKILLS + if skill.id == selected_skill_id + ), + None, + ) + + # Remember the skill selected by the user. + step_context.values[self._selected_skill_key] = selected_skill + + # Create the PromptOptions with the actions supported by the selected skill. + options = PromptOptions( + prompt=MessageFactory.text( + f"What action would you like to call in **{selected_skill.id}**?" + ), + retry_prompt=MessageFactory.text( + "That was not a valid choice, please select a valid action." + ), + choices=self._get_skill_actions(selected_skill), + style=ListStyle.suggested_action, + ) + + # Prompt the user to select a skill action. + return await step_context.prompt(ChoicePrompt.__name__, options) + + async def call_skill_action_step( + self, step_context: WaterfallStepContext + ) -> DialogTurnResult: + # Starts SkillDialog based on the user's selections + selected_skill: BotFrameworkSkill = step_context.values[ + self._selected_skill_key + ] + + if selected_skill.id == "EchoSkillBot": + # Echo skill only handles message activities, send a dummy utterance to get it started. + skill_activity = Activity( + type=ActivityTypes.message, + attachments=[], + entities=[], + text="Start echo skill", + ) + elif selected_skill.id == "DialogSkillBot": + skill_activity = self._get_dialog_skill_bot_activity(step_context.result) + else: + raise Exception(f"Unknown target skill id: {selected_skill.id}.") + + skill_dialog_args = SkillDialogArgs(selected_skill, skill_activity) + + # We are manually creating the activity to send to the skill, ensure we add the ChannelData and Properties + # from the original activity so the skill gets them. + # Note: this is not necessary if we are just forwarding the current activity from context. + skill_dialog_args.activity.channel_data = ( + step_context.context.activity.channel_data + ) + skill_dialog_args.activity.additional_properties = ( + step_context.context.activity.additional_properties + ) + + return await step_context.begin_dialog(SkillDialog.__name__, skill_dialog_args) + + async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + if step_context.result: + message = "Skill invocation complete." + message += f" Result: {encode(step_context.result)}" + await step_context.context.send_activity( + MessageFactory.text(message, input_hint=InputHints.ignoring_input) + ) + + # Restart the main dialog with a different message the second time around + return await step_context.replace_dialog( + self.initial_dialog_id, "What else can I do for you?" + ) + + # Helper method to create Choice elements for the actions supported by the skill + def _get_skill_actions(self, skill: BotFrameworkSkill) -> List[Choice]: + # Note: the bot would probably render this by readying the skill manifest + # we are just using hardcoded skill actions here for simplicity. + + choices = [] + if skill.id == "EchoSkillBot": + choices.append(Choice("Messages")) + + elif skill.id == "DialogSkillBot": + choices.append(Choice("m:some message for tomorrow")) + choices.append(Choice("BookFlight")) + choices.append(Choice("OAuthTest")) + choices.append(Choice("mv:some message with value")) + choices.append(Choice("BookFlightWithValues")) + + return choices + + # Helper method to create the activity to be sent to the DialogSkillBot + def _get_dialog_skill_bot_activity(self, selected_option: str) -> Activity: + # Note: in a real bot, the dialogArgs will be created dynamically based on the conversation + # and what each action requires, this code hardcodes the values to make things simpler. + + selected_option = selected_option.lower() + + # Send a message activity to the skill. + if selected_option.startswith("m:"): + return Activity( + type=ActivityTypes.message, + attachments=[], + entities=[], + text=selected_option[:2].strip(), + ) + + # Send a message activity to the skill with some artificial parameters in value + elif selected_option.startswith("mv:"): + return Activity( + type=ActivityTypes.message, + attachments=[], + entities=[], + text=selected_option[:3].strip(), + value=BookingDetails(destination="New York"), + ) + + # Send an event activity to the skill with "OAuthTest" in the name. + elif selected_option == "oauthtest": + return Activity(type=ActivityTypes.event, name="OAuthTest") + + # Send an event activity to the skill with "BookFlight" in the name. + elif selected_option == "bookflight": + return Activity(type=ActivityTypes.event, name="BookFlight") + + # Send an event activity to the skill "BookFlight" in the name and some testing values. + elif selected_option == "bookflightwithvalues": + return Activity( + type=ActivityTypes.event, + name="BookFlight", + value=BookingDetails(destination="New York", origin="Seattle"), + ) + + raise Exception(f'Unable to create dialogArgs for "{selected_option}".') diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/__init__.py new file mode 100644 index 000000000..699f8693c --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from . import activity_helper, luis_helper, dialog_helper + +__all__ = ["activity_helper", "dialog_helper", "luis_helper"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/activity_helper.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/activity_helper.py new file mode 100644 index 000000000..29a24823e --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/activity_helper.py @@ -0,0 +1,37 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from datetime import datetime +from botbuilder.schema import ( + Activity, + ActivityTypes, + ChannelAccount, + ConversationAccount, +) + + +def create_activity_reply(activity: Activity, text: str = None, locale: str = None): + + return Activity( + type=ActivityTypes.message, + timestamp=datetime.utcnow(), + from_property=ChannelAccount( + id=getattr(activity.recipient, "id", None), + name=getattr(activity.recipient, "name", None), + ), + recipient=ChannelAccount( + id=activity.from_property.id, name=activity.from_property.name + ), + reply_to_id=activity.id, + service_url=activity.service_url, + channel_id=activity.channel_id, + conversation=ConversationAccount( + is_group=activity.conversation.is_group, + id=activity.conversation.id, + name=activity.conversation.name, + ), + text=text or "", + locale=locale or "", + attachments=[], + entities=[], + ) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/dialog_helper.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/dialog_helper.py new file mode 100644 index 000000000..6b2646b0b --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/dialog_helper.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core import StatePropertyAccessor, TurnContext +from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus + + +class DialogHelper: + @staticmethod + async def run_dialog( + dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor + ): + dialog_set = DialogSet(accessor) + dialog_set.add(dialog) + + dialog_context = await dialog_set.create_context(turn_context) + results = await dialog_context.continue_dialog() + if results.status == DialogTurnStatus.Empty: + await dialog_context.begin_dialog(dialog.id) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/luis_helper.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/luis_helper.py new file mode 100644 index 000000000..3e28bc47e --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/luis_helper.py @@ -0,0 +1,102 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from enum import Enum +from typing import Dict +from botbuilder.ai.luis import LuisRecognizer +from botbuilder.core import IntentScore, TopIntent, TurnContext + +from booking_details import BookingDetails + + +class Intent(Enum): + BOOK_FLIGHT = "BookFlight" + CANCEL = "Cancel" + GET_WEATHER = "GetWeather" + NONE_INTENT = "NoneIntent" + + +def top_intent(intents: Dict[Intent, dict]) -> TopIntent: + max_intent = Intent.NONE_INTENT + max_value = 0.0 + + for intent, value in intents: + intent_score = IntentScore(value) + if intent_score.score > max_value: + max_intent, max_value = intent, intent_score.score + + return TopIntent(max_intent, max_value) + + +class LuisHelper: + @staticmethod + async def execute_luis_query( + luis_recognizer: LuisRecognizer, turn_context: TurnContext + ) -> (Intent, object): + """ + Returns an object with preformatted LUIS results for the bot's dialogs to consume. + """ + result = None + intent = None + + try: + recognizer_result = await luis_recognizer.recognize(turn_context) + + intent = ( + sorted( + recognizer_result.intents, + key=recognizer_result.intents.get, + reverse=True, + )[:1][0] + if recognizer_result.intents + else None + ) + + if intent == Intent.BOOK_FLIGHT.value: + result = BookingDetails() + + # We need to get the result from the LUIS JSON which at every level returns an array. + to_entities = recognizer_result.entities.get("$instance", {}).get( + "To", [] + ) + if len(to_entities) > 0: + if recognizer_result.entities.get("To", [{"$instance": {}}])[0][ + "$instance" + ]: + result.destination = to_entities[0]["text"].capitalize() + else: + result.unsupported_airports.append( + to_entities[0]["text"].capitalize() + ) + + from_entities = recognizer_result.entities.get("$instance", {}).get( + "From", [] + ) + if len(from_entities) > 0: + if recognizer_result.entities.get("From", [{"$instance": {}}])[0][ + "$instance" + ]: + result.origin = from_entities[0]["text"].capitalize() + else: + result.unsupported_airports.append( + from_entities[0]["text"].capitalize() + ) + + # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop + # the Time part. TIMEX is a format that represents DateTime expressions that include some ambiguity. + # e.g. missing a Year. + date_entities = recognizer_result.entities.get("datetime", []) + if date_entities: + timex = date_entities[0]["timex"] + + if timex: + datetime = timex[0].split("T")[0] + + result.travel_date = datetime + + else: + result.travel_date = None + + except Exception as exception: + print(exception) + + return intent, result diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/__init__.py new file mode 100644 index 000000000..c23b52ce2 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/__init__.py @@ -0,0 +1,4 @@ +from .dummy_middleware import DummyMiddleware + + +__all__ = ["DummyMiddleware"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/dummy_middleware.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/dummy_middleware.py new file mode 100644 index 000000000..4d38fe79f --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/dummy_middleware.py @@ -0,0 +1,32 @@ +from typing import Awaitable, Callable, List + +from botbuilder.core import Middleware, TurnContext +from botbuilder.schema import Activity, ResourceResponse + + +class DummyMiddleware(Middleware): + def __init__(self, label: str): + self._label = label + + async def on_turn( + self, context: TurnContext, logic: Callable[[TurnContext], Awaitable] + ): + message = f"{self._label} {context.activity.type} {context.activity.text}" + print(message) + + # Register outgoing handler + context.on_send_activities(self._outgoing_handler) + + await logic() + + async def _outgoing_handler( + self, + context: TurnContext, # pylint: disable=unused-argument + activities: List[Activity], + logic: Callable[[TurnContext], Awaitable[List[ResourceResponse]]], + ): + for activity in activities: + message = f"{self._label} {activity.type} {activity.text}" + print(message) + + return await logic() From 91d7c6b7dcc468e57b609f44cd8f7f6226d95a69 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 21 Feb 2020 17:39:11 -0800 Subject: [PATCH 06/38] pylint: made integration package for aiohttp, dialog root bot for testing --- .../integration/aiohttp/bot_framework_http_client.py | 7 +++---- .../integration/aiohttp/skills/skill_http_client.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py index 74a1fdfce..62f400326 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py @@ -1,11 +1,13 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import aiohttp import json from typing import Dict from logging import Logger -import aiohttp +from botbuilder.core import InvokeResponse +from botbuilder.core.skills import BotFrameworkClient from botbuilder.schema import Activity from botframework.connector.auth import ( ChannelProvider, @@ -14,9 +16,6 @@ MicrosoftAppCredentials, ) -from botbuilder.core import InvokeResponse -from botbuilder.core.skills import BotFrameworkClient - class BotFrameworkHttpClient(BotFrameworkClient): diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py index b27a4058b..6f6c4910c 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py @@ -1,6 +1,5 @@ from logging import Logger -from botbuilder.schema import Activity from botframework.connector.auth import ( ChannelProvider, CredentialProvider, @@ -8,6 +7,7 @@ from botbuilder.core import InvokeResponse, TurnContext from botbuilder.core.skills import BotFrameworkSkill, SkillConversationIdFactory from botbuilder.integration.aiohttp import BotFrameworkHttpClient +from botbuilder.schema import Activity class SkillHttpClient(BotFrameworkHttpClient): From 10117aa7b058e36b5601a26a4903fd459009cce6 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 21 Feb 2020 17:40:07 -0800 Subject: [PATCH 07/38] pylint: made integration package for aiohttp, dialog root bot for testing --- .../botbuilder/integration/aiohttp/bot_framework_http_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py index 62f400326..6528b0de8 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py @@ -1,11 +1,11 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import aiohttp import json from typing import Dict from logging import Logger +import aiohttp from botbuilder.core import InvokeResponse from botbuilder.core.skills import BotFrameworkClient from botbuilder.schema import Activity From 4a2ef034dadeba0824876485aadfc3adc5a2790f Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 21 Feb 2020 17:42:07 -0800 Subject: [PATCH 08/38] pylint: made integration package for aiohttp, dialog root bot for testing --- .../botbuilder/integration/aiohttp/bot_framework_http_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py index 6528b0de8..13f250adc 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +# pylint: disable=no-member import json from typing import Dict From 0c920830e9f4d8394098d6b5a14296582665d2b3 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Sat, 22 Feb 2020 15:37:51 -0800 Subject: [PATCH 09/38] Initial dialog skill bot --- .../dialog-skill-bot/README.md | 38 +++ .../dialog-to-dialog/dialog-skill-bot/app.py | 66 +++++ .../authentication/__init__.py | 3 + .../allow_callers_claims_validation.py | 39 +++ .../dialog-skill-bot/bots/__init__.py | 6 + .../bots/activity_router_dialog.py | 169 +++++++++++ .../dialog-skill-bot/bots/skill_bot.py | 19 ++ .../dialog-skill-bot/config.py | 17 ++ .../template-with-new-rg.json | 264 ++++++++++++++++++ .../template-with-preexisting-rg.json | 242 ++++++++++++++++ .../dialog-skill-bot/dialogs/__init__.py | 18 ++ .../dialogs/booking_details.py | 24 ++ .../dialogs/booking_dialog.py | 136 +++++++++ .../dialogs/cancel_and_help_dialog.py | 47 ++++ .../dialogs/date_resolver_dialog.py | 82 ++++++ .../dialogs/dialog_skill_bot_recognizer.py | 34 +++ .../dialog-skill-bot/dialogs/location.py | 16 ++ .../dialogs/oauth_test_dialog.py | 89 ++++++ .../dialog-skill-bot/requirements.txt | 2 + .../skill_adapter_with_error_handler.py | 92 ++++++ 20 files changed, 1403 insertions(+) create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/README.md create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/app.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/__init__.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/allow_callers_claims_validation.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/__init__.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-new-rg.json create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-preexisting-rg.json create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/__init__.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_details.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_dialog.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/cancel_and_help_dialog.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/date_resolver_dialog.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/dialog_skill_bot_recognizer.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/location.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/oauth_test_dialog.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/requirements.txt create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/skill_adapter_with_error_handler.py diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/README.md b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/README.md new file mode 100644 index 000000000..299259d5b --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/README.md @@ -0,0 +1,38 @@ +# EchoBot + +Bot Framework v4 echo bot sample. + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back. + +## Running the sample +- Clone the repository +```bash +git clone https://github.com/Microsoft/botbuilder-samples.git +``` +- Bring up a terminal, navigate to `botbuilder-samples\samples\python\02.echo-bot` folder +- Activate your desired virtual environment +- In the terminal, type `pip install -r requirements.txt` +- Run your bot with `python app.py` + +## Testing the bot using Bot Framework Emulator +[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + +- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + +### Connect to bot using Bot Framework Emulator +- Launch Bot Framework Emulator +- Paste this URL in the emulator window - http://localhost:3978/api/messages + +## Deploy the bot to Azure + +To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions. + +# Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) +- [Azure Portal](https://portal.azure.com) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/app.py new file mode 100644 index 000000000..17a9410a1 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/app.py @@ -0,0 +1,66 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from aiohttp import web +from aiohttp.web import Request, Response, json_response +from botbuilder.core import ( + BotFrameworkAdapterSettings, + ConversationState, + MemoryStorage, + UserState, +) +from botbuilder.core.integration import aiohttp_error_middleware +from botbuilder.schema import Activity +from botframework.connector.auth import AuthenticationConfiguration + +from authentication import AllowedCallersClaimsValidator +from bots import EchoBot +from config import DefaultConfig +from skill_adapter_with_error_handler import SkillAdapterWithErrorHandler + +CONFIG = DefaultConfig() + +# Create MemoryStorage, UserState and ConversationState +MEMORY = MemoryStorage() +USER_STATE = UserState(MEMORY) +CONVERSATION_STATE = ConversationState(MEMORY) + +# Create adapter. +# See https://aka.ms/about-bot-adapter to learn more about how bots work. +VALIDATOR = AllowedCallersClaimsValidator(CONFIG).claims_validator +SETTINGS = BotFrameworkAdapterSettings( + CONFIG.APP_ID, + CONFIG.APP_PASSWORD, + auth_configuration=AuthenticationConfiguration(claims_validator=VALIDATOR), +) +ADAPTER = SkillAdapterWithErrorHandler(SETTINGS, CONVERSATION_STATE) + +# Create the Bot +BOT = EchoBot() + + +# Listen for incoming requests on /api/messages +async def messages(req: Request) -> Response: + # Main bot message handler. + if "application/json" in req.headers["Content-Type"]: + body = await req.json() + else: + return Response(status=415) + + activity = Activity().deserialize(body) + auth_header = req.headers["Authorization"] if "Authorization" in req.headers else "" + + response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn) + if response: + return json_response(data=response.body, status=response.status) + return Response(status=201) + + +APP = web.Application(middlewares=[aiohttp_error_middleware]) +APP.router.add_post("/api/messages", messages) + +if __name__ == "__main__": + try: + web.run_app(APP, host="localhost", port=CONFIG.PORT) + except Exception as error: + raise error diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/__init__.py new file mode 100644 index 000000000..ebbe2ac15 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/__init__.py @@ -0,0 +1,3 @@ +from .allow_callers_claims_validation import AllowedCallersClaimsValidator + +__all__ = ["AllowedCallersClaimsValidator"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/allow_callers_claims_validation.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/allow_callers_claims_validation.py new file mode 100644 index 000000000..2d5b30060 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/allow_callers_claims_validation.py @@ -0,0 +1,39 @@ +from typing import Awaitable, Callable, Dict, List + +from botframework.connector.auth import JwtTokenValidation, SkillValidation + +from ..config import DefaultConfig + + +class AllowedCallersClaimsValidator: + + config_key = "ALLOWED_CALLERS" + + def __init__(self, config: DefaultConfig): + if not config: + raise TypeError( + "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 + self._allowed_callers = getattr(config) + + @property + def claims_validator(self) -> Callable[[List[Dict]], Awaitable]: + async def allow_callers_claims_validator(claims: Dict[str, object]): + # if _allowedCallers is None we allow all calls + if 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. + app_id = JwtTokenValidation.get_app_id_from_claims(claims) + if app_id not in self._allowed_callers: + raise PermissionError( + f'Received a request from a bot with an app ID of "{app_id}".' + f" To enable requests from this caller, add the app ID to your configuration file." + ) + + return + + return allow_callers_claims_validator diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/__init__.py new file mode 100644 index 000000000..f95fbbbad --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .echo_bot import EchoBot + +__all__ = ["EchoBot"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py new file mode 100644 index 000000000..b2816236a --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py @@ -0,0 +1,169 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import List + +from jsonpickle import encode +from botbuilder.dialogs import ( + ComponentDialog, + WaterfallDialog, + WaterfallStepContext, + DialogTurnResult, + DialogTurnStatus +) +from botbuilder.dialogs.choices import Choice, ListStyle +from botbuilder.dialogs.prompts import PromptOptions, ChoicePrompt +from botbuilder.dialogs.skills import SkillDialogOptions, SkillDialog, SkillDialogArgs +from botbuilder.core import ConversationState, MessageFactory, InvokeResponse +from botbuilder.core.skills import BotFrameworkSkill, ConversationIdFactoryBase +from botbuilder.schema import Activity, ActivityTypes, InputHints +from botbuilder.integration.aiohttp.skills import SkillHttpClient + +from ..config import DefaultConfig, SkillConfiguration +from ..dialogs import BookingDetails, BookingDialog, DialogSkillBotRecognizer, Location, OAuthTestDialog + + +class ActivityRouterDialog(ComponentDialog): + def __init__( + self, luis_recognizer: DialogSkillBotRecognizer, configuration: DefaultConfig + ): + super().__init__(ActivityRouterDialog.__name__) + + self._luis_recognizer = luis_recognizer + + self.add_dialog(BookingDialog()) + self.add_dialog(OAuthTestDialog(configuration)) + self.add_dialog(WaterfallDialog(WaterfallDialog.__name__, [None])) + + self.initial_dialog_id = WaterfallDialog.__name__ + + async def process_activity( + self, step_context: WaterfallStepContext + ) -> DialogTurnResult: + current_activity_type = step_context.context.activity.type + + # A skill can send trace activities if needed + await step_context.context.send_trace_activity( + f"{ActivityRouterDialog.__name__}.process_activity()", + label=f"Got ActivityType: {current_activity_type}", + ) + + if current_activity_type == ActivityTypes.message: + return await self._on_message_activity(step_context) + elif current_activity_type == ActivityTypes.invoke: + return await self._on_invoke_activity(step_context) + elif current_activity_type == ActivityTypes.event: + return await self._on_event_activity(step_context) + + # We didn't get an activity type we can handle. + await step_context.context.send_activity( + MessageFactory.text( + f'Unrecognized ActivityType: "{current_activity_type}".', + input_hint=InputHints.ignoring_input, + ) + ) + return DialogTurnResult(DialogTurnStatus.Complete) + + async def _on_event_activity( + self, step_context: WaterfallStepContext + ) -> DialogTurnResult: + activity = step_context.context.activity + + # Resolve what to execute based on the event name. + if activity.name == "BookFlight": + booking_details = BookingDetails() + if activity.value: + booking_details.from_json(activity.value) + + # Start the booking dialog + booking_dialog = self.find_dialog(BookingDialog.__name__) + return await step_context.begin_dialog(booking_dialog.id, booking_details) + elif activity.name == "OAuthTest": + # Start the oauth dialog + oauth_dialog = self.find_dialog(OAuthTestDialog.__name__) + return await step_context.begin_dialog(oauth_dialog.id, None) + + else: + await step_context.context.send_activity( + MessageFactory.text( + f'Unrecognized EventName: "{activity.name}".', + input_hint=InputHints.ignoring_input, + ) + ) + return DialogTurnResult(DialogTurnStatus.Complete) + + async def _on_invoke_activity( + self, step_context: WaterfallStepContext + ) -> DialogTurnResult: + activity = step_context.context.activity + + # Resolve what to execute based on the event name. + if activity.name == "GetWeather": + location = Location() + if activity.value: + location.from_json(activity.value) + + looking_into_it_message = "Getting your weather forecast..." + step_context.context.send_activity(MessageFactory.text(looking_into_it_message, looking_into_it_message, InputHints.ignoring_input)) + + # Create and return an invoke activity with the weather results. + invoke_response_activity = Activity( + type="invokeResponse", + value=InvokeResponse( + body=[ + "New York, NY, Clear, 56 F", + "Bellevue, WA, Mostly Cloudy, 48 F" + ], + status=200 + ) + ) + + await step_context.context.send_activity(invoke_response_activity) + else: + await step_context.context.send_activity( + MessageFactory.text( + f'Unrecognized InvokeName: "{activity.name}".', + input_hint=InputHints.ignoring_input, + ) + ) + + return DialogTurnResult(DialogTurnStatus.Complete) + + async def _on_message_activity( + self, step_context: WaterfallStepContext + ) -> DialogTurnResult: + activity = step_context.context.activity + + if not self._luis_recognizer.is_configured: + await step_context.context.send_activity( + MessageFactory.text( + "NOTE: LUIS is not configured. To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and" + " 'LuisAPIHostName' to the appsettings.json file.", + input_hint=InputHints.ignoring_input, + ) + ) + else: + # Call LUIS with the utterance. + luis_result = await self._luis_recognizer.recognize(step_context.context) + + message = f"LUIS results for \"{activity.Text}\":\n" + intent, intent_score = None, None + if luis_result.intents: + max_value_key = max(luis_result.intents, key=lambda key: luis_result.intents[key]) + intent, intent_score = max_value_key, luis_result.intents[max_value_key] + + message += f"Intent: \"{intent}\" Score: {intent_score}\n" + message += f"Entities found: {len(luis_result.entities) - 1}\n" + for entity_key, entity_val in luis_result.entities: + if not entity_key == "$instance": + message += f"* {entity_val}\n" + + await step_context.context.send_activity( + MessageFactory.text( + message, + input_hint=InputHints.ignoring_input, + ) + ) + + return DialogTurnResult(DialogTurnStatus.Complete) + diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py new file mode 100644 index 000000000..90a094640 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core import ActivityHandler, MessageFactory, TurnContext +from botbuilder.schema import ChannelAccount + + +class EchoBot(ActivityHandler): + async def on_members_added_activity( + self, members_added: [ChannelAccount], turn_context: TurnContext + ): + for member in members_added: + if member.id != turn_context.activity.recipient.id: + await turn_context.send_activity("Hello and welcome!") + + async def on_message_activity(self, turn_context: TurnContext): + return await turn_context.send_activity( + MessageFactory.text(f"Echo: {turn_context.activity.text}") + ) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py new file mode 100644 index 000000000..488a46631 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + +""" Bot Configuration """ + + +class DefaultConfig: + """ Bot Configuration """ + + PORT = 3978 + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") + + CONNECTION_NAME = "" diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-new-rg.json b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 000000000..b68e51ec6 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-new-rg.json @@ -0,0 +1,264 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "newAppServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan. Defaults to \"westus\"." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "appServicePlanName": "[parameters('newAppServicePlanName')]", + "resourcesLocation": "[parameters('newAppServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]" + }, + "resources": [ + { + "name": "[parameters('groupName')]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('groupLocation')]", + "properties": { + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('groupName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "name": "[variables('appServicePlanName')]", + "perSiteScaling": false, + "reserved": true, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app,linux", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[variables('appServicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", + "value": "true" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2016-08-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "phpVersion": "", + "pythonVersion": "", + "nodeVersion": "", + "linuxFxVersion": "PYTHON|3.7", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "remoteDebuggingVersion": "VS2017", + "httpLoggingEnabled": true, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": false, + "appCommandLine": "gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:APP", + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": false, + "virtualDirectories": null + } + ], + "winAuthAdminState": 0, + "winAuthTenantState": 0, + "customAppPoolIdentityAdminState": false, + "customAppPoolIdentityTenantState": false, + "loadBalancing": "LeastRequests", + "routingRules": [], + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "vnetName": "", + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ], + "outputs": {} + } + } + } + ] +} \ No newline at end of file diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-preexisting-rg.json b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..66b044b52 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,242 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "F0", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2016-09-01", + "name": "[variables('servicePlanName')]", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "name": "[variables('servicePlanName')]", + "perSiteScaling": false, + "reserved": true, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2016-08-01", + "name": "[variables('webAppName')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" + ], + "kind": "app,linux", + "properties": { + "enabled": true, + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "reserved": true, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": false, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "siteConfig": { + "appSettings": [ + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + }, + { + "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", + "value": "true" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2016-08-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "phpVersion": "", + "pythonVersion": "", + "nodeVersion": "", + "linuxFxVersion": "PYTHON|3.7", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "remoteDebuggingVersion": "VS2017", + "httpLoggingEnabled": true, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": false, + "appCommandLine": "gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:APP", + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": false, + "virtualDirectories": null + } + ], + "winAuthAdminState": 0, + "winAuthTenantState": 0, + "customAppPoolIdentityAdminState": false, + "customAppPoolIdentityTenantState": false, + "loadBalancing": "LeastRequests", + "routingRules": [], + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "vnetName": "", + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} \ No newline at end of file diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/__init__.py new file mode 100644 index 000000000..b6282e719 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/__init__.py @@ -0,0 +1,18 @@ +from .booking_details import BookingDetails +from .booking_dialog import BookingDialog +from .cancel_and_help_dialog import CancelAndHelpDialog +from .date_resolver_dialog import DateResolverDialog +from .dialog_skill_bot_recognizer import DialogSkillBotRecognizer +from .location import Location +from .oauth_test_dialog import OAuthTestDialog + + +__all__ = [ + "BookingDetails", + "BookingDialog", + "CancelAndHelpDialog", + "DateResolverDialog", + "DialogSkillBotRecognizer", + "Location", + "OAuthTestDialog", +] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_details.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_details.py new file mode 100644 index 000000000..3e0a1012f --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_details.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class BookingDetails: + def __init__( + self, + destination: str = None, + origin: str = None, + travel_date: str = None, + unsupported_airports=None, + ): + if unsupported_airports is None: + unsupported_airports = [] + self.destination = destination + self.origin = origin + self.travel_date = travel_date + self.unsupported_airports = unsupported_airports + + def from_json(self, json: dict): + self.destination = json.get("destination", None) + self.origin = json.get("origin", None) + self.travel_date = json.get("travel_date", None) + self.unsupported_airports = json.get("unsupported_airports", []) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_dialog.py new file mode 100644 index 000000000..5b4381919 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_dialog.py @@ -0,0 +1,136 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from datatypes_date_time.timex import Timex + +from botbuilder.dialogs import WaterfallDialog, WaterfallStepContext, DialogTurnResult +from botbuilder.dialogs.prompts import ConfirmPrompt, TextPrompt, PromptOptions +from botbuilder.core import MessageFactory +from botbuilder.schema import InputHints +from .cancel_and_help_dialog import CancelAndHelpDialog +from .date_resolver_dialog import DateResolverDialog + + +class BookingDialog(CancelAndHelpDialog): + def __init__(self, dialog_id: str = None): + super(BookingDialog, self).__init__(dialog_id or BookingDialog.__name__) + + self.add_dialog(TextPrompt(TextPrompt.__name__)) + self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__)) + self.add_dialog(DateResolverDialog(DateResolverDialog.__name__)) + self.add_dialog( + WaterfallDialog( + WaterfallDialog.__name__, + [ + self.destination_step, + self.origin_step, + self.travel_date_step, + self.confirm_step, + self.final_step, + ], + ) + ) + + self.initial_dialog_id = WaterfallDialog.__name__ + + async def destination_step( + self, step_context: WaterfallStepContext + ) -> DialogTurnResult: + """ + If a destination city has not been provided, prompt for one. + :param step_context: + :return DialogTurnResult: + """ + booking_details = step_context.options + + if booking_details.destination is None: + message_text = "Where would you like to travel to?" + prompt_message = MessageFactory.text( + message_text, message_text, InputHints.expecting_input + ) + return await step_context.prompt( + TextPrompt.__name__, PromptOptions(prompt=prompt_message) + ) + return await step_context.next(booking_details.destination) + + async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """ + If an origin city has not been provided, prompt for one. + :param step_context: + :return DialogTurnResult: + """ + booking_details = step_context.options + + # Capture the response to the previous step's prompt + booking_details.destination = step_context.result + if booking_details.origin is None: + message_text = "From what city will you be travelling?" + prompt_message = MessageFactory.text( + message_text, message_text, InputHints.expecting_input + ) + return await step_context.prompt( + TextPrompt.__name__, PromptOptions(prompt=prompt_message) + ) + return await step_context.next(booking_details.origin) + + async def travel_date_step( + self, step_context: WaterfallStepContext + ) -> DialogTurnResult: + """ + If a travel date has not been provided, prompt for one. + This will use the DATE_RESOLVER_DIALOG. + :param step_context: + :return DialogTurnResult: + """ + booking_details = step_context.options + + # Capture the results of the previous step + booking_details.origin = step_context.result + if not booking_details.travel_date or self.is_ambiguous( + booking_details.travel_date + ): + return await step_context.begin_dialog( + DateResolverDialog.__name__, booking_details.travel_date + ) + return await step_context.next(booking_details.travel_date) + + async def confirm_step( + self, step_context: WaterfallStepContext + ) -> DialogTurnResult: + """ + Confirm the information the user has provided. + :param step_context: + :return DialogTurnResult: + """ + booking_details = step_context.options + + # Capture the results of the previous step + booking_details.travel_date = step_context.result + message_text = ( + f"Please confirm, I have you traveling to: { booking_details.destination } from: " + f"{ booking_details.origin } on: { booking_details.travel_date}." + ) + prompt_message = MessageFactory.text( + message_text, message_text, InputHints.expecting_input + ) + + # Offer a YES/NO prompt. + return await step_context.prompt( + ConfirmPrompt.__name__, PromptOptions(prompt=prompt_message) + ) + + async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """ + Complete the interaction and end the dialog. + :param step_context: + :return DialogTurnResult: + """ + if step_context.result: + booking_details = step_context.options + + return await step_context.end_dialog(booking_details) + return await step_context.end_dialog() + + def is_ambiguous(self, timex: str) -> bool: + timex_property = Timex(timex) + return "definite" not in timex_property.types diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/cancel_and_help_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/cancel_and_help_dialog.py new file mode 100644 index 000000000..f8bcc77d0 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/cancel_and_help_dialog.py @@ -0,0 +1,47 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.dialogs import ( + ComponentDialog, + DialogContext, + DialogTurnResult, + DialogTurnStatus, +) +from botbuilder.schema import ActivityTypes, InputHints +from botbuilder.core import MessageFactory + + +class CancelAndHelpDialog(ComponentDialog): + def __init__(self, dialog_id: str): + super(CancelAndHelpDialog, self).__init__(dialog_id) + + async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: + result = await self.interrupt(inner_dc) + if result is not None: + return result + + return await super(CancelAndHelpDialog, self).on_continue_dialog(inner_dc) + + 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 = MessageFactory.text( + help_message_text, help_message_text, InputHints.expecting_input + ) + + if text in ("help", "?"): + await inner_dc.context.send_activity(help_message) + return DialogTurnResult(DialogTurnStatus.Waiting) + + cancel_message_text = "Cancelling" + cancel_message = MessageFactory.text( + cancel_message_text, cancel_message_text, InputHints.ignoring_input + ) + + if text in ("cancel", "quit"): + await inner_dc.context.send_activity(cancel_message) + return await inner_dc.cancel_all_dialogs() + + return None diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/date_resolver_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/date_resolver_dialog.py new file mode 100644 index 000000000..c5c50463d --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/date_resolver_dialog.py @@ -0,0 +1,82 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from datatypes_date_time.timex import Timex + +from botbuilder.core import MessageFactory +from botbuilder.dialogs import WaterfallDialog, DialogTurnResult, WaterfallStepContext +from botbuilder.dialogs.prompts import ( + DateTimePrompt, + PromptValidatorContext, + PromptOptions, + DateTimeResolution, +) +from botbuilder.schema import InputHints +from .cancel_and_help_dialog import CancelAndHelpDialog + + +class DateResolverDialog(CancelAndHelpDialog): + def __init__(self, dialog_id: str = None): + super(DateResolverDialog, self).__init__( + dialog_id or DateResolverDialog.__name__ + ) + + self.add_dialog( + DateTimePrompt( + DateTimePrompt.__name__, DateResolverDialog.datetime_prompt_validator + ) + ) + self.add_dialog( + WaterfallDialog( + WaterfallDialog.__name__ + "2", [self.initial_step, self.final_step] + ) + ) + + self.initial_dialog_id = WaterfallDialog.__name__ + "2" + + async def initial_step( + self, step_context: WaterfallStepContext + ) -> DialogTurnResult: + timex = step_context.options + + prompt_msg_text = "On what date would you like to travel?" + prompt_msg = MessageFactory.text( + prompt_msg_text, prompt_msg_text, InputHints.expecting_input + ) + + reprompt_msg_text = ( + "I'm sorry, for best results, please enter your travel date including the month, " + "day and year. " + ) + reprompt_msg = MessageFactory.text( + reprompt_msg_text, reprompt_msg_text, InputHints.expecting_input + ) + + if timex is None: + # We were not given any date at all so prompt the user. + return await step_context.prompt( + DateTimePrompt.__name__, + PromptOptions(prompt=prompt_msg, retry_prompt=reprompt_msg), + ) + # We have a Date we just need to check it is unambiguous. + if "definite" not in Timex(timex).types: + # This is essentially a "reprompt" of the data we were given up front. + return await step_context.prompt( + DateTimePrompt.__name__, PromptOptions(prompt=reprompt_msg) + ) + + return await step_context.next(DateTimeResolution(timex=timex)) + + async def final_step(self, step_context: WaterfallStepContext): + timex = step_context.result[0].timex + return await step_context.end_dialog(timex) + + @staticmethod + async def datetime_prompt_validator(prompt_context: PromptValidatorContext) -> bool: + if prompt_context.recognized.succeeded: + timex = prompt_context.recognized.value[0].timex.split("T")[0] + + # TODO: Needs TimexProperty + return "definite" in Timex(timex).types + + return False diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/dialog_skill_bot_recognizer.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/dialog_skill_bot_recognizer.py new file mode 100644 index 000000000..6e2bb5018 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/dialog_skill_bot_recognizer.py @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.ai.luis import LuisApplication, LuisRecognizer +from botbuilder.core import Recognizer, RecognizerResult, TurnContext + +from ..config import DefaultConfig + + +class DialogSkillBotRecognizer(Recognizer): + def __init__(self, configuration: DefaultConfig): + self._recognizer = None + + luis_is_configured = ( + configuration.LUIS_APP_ID + and configuration.LUIS_API_KEY + and configuration.LUIS_API_HOST_NAME + ) + if luis_is_configured: + luis_application = LuisApplication( + configuration.LUIS_APP_ID, + configuration.LUIS_API_KEY, + "https://" + configuration.LUIS_API_HOST_NAME, + ) + + self._recognizer = LuisRecognizer(luis_application) + + @property + def is_configured(self) -> bool: + # Returns true if luis is configured in the config.py and initialized. + return self._recognizer is not None + + async def recognize(self, turn_context: TurnContext) -> RecognizerResult: + return await self._recognizer.recognize(turn_context) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/location.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/location.py new file mode 100644 index 000000000..3ee3406ea --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/location.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class Location: + def __init__( + self, latitude: str = None, longitude: str = None, postal_code: str = None, + ): + self.latitude = latitude + self.longitude = longitude + self.postal_code = postal_code + + def from_json(self, json: dict): + self.latitude = json.get("latitude", None) + self.longitude = json.get("longitude", None) + self.postal_code = json.get("postal_code", None) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/oauth_test_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/oauth_test_dialog.py new file mode 100644 index 000000000..d0258c357 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/oauth_test_dialog.py @@ -0,0 +1,89 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.dialogs import ( + WaterfallDialog, + WaterfallStepContext, + DialogTurnResult, +) +from botbuilder.dialogs.prompts import ( + ConfirmPrompt, + PromptOptions, + OAuthPrompt, + OAuthPromptSettings, +) +from botbuilder.core import BotFrameworkAdapter, MessageFactory +from botbuilder.schema import InputHints + +from .cancel_and_help_dialog import CancelAndHelpDialog +from ..config import DefaultConfig + + +class OAuthTestDialog(CancelAndHelpDialog): + def __init__(self, configuration: DefaultConfig): + super().__init__(OAuthTestDialog.__name__) + + self._connection_name = configuration.CONNECTION_NAME + + self.add_dialog( + OAuthPrompt( + OAuthPrompt.__name__, + OAuthPromptSettings( + connection_name=self._connection_name, + text=f"Please Sign In to connection: '{self._connection_name}'", + title="Sign In", + timeout=300000, + ), + ) + ) + + self.add_dialog( + WaterfallDialog( + WaterfallDialog.__name__, [self.prompt_step, self.login_step] + ) + ) + + self.initial_dialog_id = WaterfallDialog.__name__ + + async def prompt_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + return await step_context.begin_dialog(OAuthPrompt.__name__) + + async def login_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + # Get the token from the previous step. + token_response = step_context.result + if token_response: + # Show the token + logged_in_message = "You are now logged in." + show_token_message = "Here is your token:" + await step_context.context.send_activity( + MessageFactory.text( + logged_in_message, logged_in_message, InputHints.ignoring_input + ) + ) + await step_context.context.send_activity( + MessageFactory.text( + f"{show_token_message} {token_response.token}", + show_token_message, + InputHints.ignoring_input, + ) + ) + + # Sign out + bot_adapter: BotFrameworkAdapter = step_context.context.adapter + await bot_adapter.sign_out_user(step_context.context, self._connection_name) + sign_out_message = "You have been signed out." + await step_context.context.send_activity( + MessageFactory.text( + sign_out_message, sign_out_message, InputHints.ignoring_input + ) + ) + + return await step_context.end_dialog() + + try_again_message = "Login was not successful please try again." + await step_context.context.send_activity( + MessageFactory.text( + try_again_message, try_again_message, InputHints.ignoring_input + ) + ) + return await step_context.end_dialog() diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/requirements.txt b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/requirements.txt new file mode 100644 index 000000000..52eb5fe1e --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/requirements.txt @@ -0,0 +1,2 @@ +aiohttp +botbuilder-core>=4.7.0 diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/skill_adapter_with_error_handler.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/skill_adapter_with_error_handler.py new file mode 100644 index 000000000..d7cf87c73 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/skill_adapter_with_error_handler.py @@ -0,0 +1,92 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import sys +import traceback +from datetime import datetime + +from botbuilder.core import ( + BotFrameworkAdapter, + BotFrameworkAdapterSettings, + ConversationState, + MessageFactory, + TurnContext, +) +from botbuilder.schema import ActivityTypes, Activity, InputHints + + +class SkillAdapterWithErrorHandler(BotFrameworkAdapter): + def __init__( + self, + settings: BotFrameworkAdapterSettings, + conversation_state: ConversationState = None, + ): + super().__init__(settings) + self._conversation_state = conversation_state + + # Catch-all for errors. + async def on_error(context: TurnContext, error: Exception): + # This check writes out errors to console log + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() + + # Send a message to the user + error_message_text = "The skill encountered an error or bug." + error_message = MessageFactory.text( + error_message_text, error_message_text, InputHints.ignoring_input + ) + await context.send_activity(error_message) + + error_message_text = ( + "To continue to run this bot, please fix the bot source code." + ) + error_message = MessageFactory.text( + error_message_text, error_message_text, InputHints.ignoring_input + ) + await context.send_activity(error_message) + + # Send a trace activity if we're talking to the Bot Framework Emulator + if context.activity.channel_id == "emulator": + # Create a trace activity that contains the error object + trace_activity = Activity( + label="TurnError", + name="on_turn_error Trace", + timestamp=datetime.utcnow(), + type=ActivityTypes.trace, + value=f"{error}", + value_type="https://www.botframework.com/schemas/error", + ) + # Send a trace activity, which will be displayed in Bot Framework Emulator + await context.send_activity(trace_activity) + + # Clear out state + nonlocal self + if self._conversation_state: + try: + await self._conversation_state.delete(context) + except Exception as exception: + print( + f"\n Exception caught on attempting to Delete ConversationState : {exception}", + file=sys.stderr, + ) + traceback.print_exc() + + # Send and EndOfConversation activity to the skill caller with the error to end the conversation + # and let the caller decide what to do. + end_of_conversation = Activity(type=ActivityTypes.end_of_conversation) + end_of_conversation.code = "SkillError" + end_of_conversation.text = str(error) + await context.send_activity(end_of_conversation) + + # Send a trace activity, which will be displayed in the Bot Framework Emulator + # Note: we return the entire exception in the value property to help the developer, + # this should not be done in prod. + await context.send_trace_activity( + "OnTurnError Trace", + str(error), + "https://www.botframework.com/schemas/error", + "TurnError", + ) + + self.on_turn_error = on_error From 9d95a3e9303dfbeadbde9b8277b0d9b48d010d34 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Mon, 24 Feb 2020 11:24:31 -0800 Subject: [PATCH 10/38] Changes to skill bot --- .../dialog-skill-bot/bots/skill_bot.py | 77 +++++++++++++++---- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py index 90a094640..15c9e4ee9 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py @@ -1,19 +1,64 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from botbuilder.core import ActivityHandler, MessageFactory, TurnContext -from botbuilder.schema import ChannelAccount - - -class EchoBot(ActivityHandler): - async def on_members_added_activity( - self, members_added: [ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity("Hello and welcome!") - - async def on_message_activity(self, turn_context: TurnContext): - return await turn_context.send_activity( - MessageFactory.text(f"Echo: {turn_context.activity.text}") - ) +import platform + +from botbuilder.core import Bot, ConversationState, MessageFactory, TurnContext +from botbuilder.dialogs import Dialog, DialogContext, DialogSet, DialogTurnStatus +from botbuilder.schema import Activity, ActivityTypes, InputHints + + +class SkillBot(Bot): + def __init__(self, conversation_state: ConversationState, main_dialog: Dialog): + self._conversation_state = conversation_state + self._main_dialog = main_dialog + + async def on_turn(self, context: TurnContext): + dialog_set = DialogSet(self._conversation_state.create_property("DialogState")) + dialog_set.add(self._main_dialog) + + dialog_context = await dialog_set.create_context(context) + if context.activity.type == ActivityTypes.end_of_conversation and dialog_context.stack: + # Handle remote cancellation request if we have something in the stack. + 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. + await active_dialog_context.cancel_all_dialogs() + remote_cancel_text = "**SkillBot.** The current mainDialog in the skill was **canceled** by a " \ + "request **from the host**, do some cleanup here if needed." + await context.send_activity(MessageFactory.text(remote_cancel_text, input_hint=InputHints.ignoring_input)) + else: + # Run the Dialog with the new message Activity and capture the results so we can send end of + # conversation if needed. + result = await dialog_context.continue_dialog() + if result.status == DialogTurnStatus.Empty: + start_message_text = f"**SkillBot.** Starting {self._main_dialog.id} " \ + f"(Pyhton {platform.python_version()})." + + await context.send_activity(MessageFactory.text( + start_message_text, + input_hint=InputHints.ignoring_input + )) + result = await dialog_context.begin_dialog(self._main_dialog.id) + + # Send end of conversation if it is complete + if result.status == DialogTurnStatus.Complete or result.status == DialogTurnStatus.Cancelled: + end_message_text = "**SkillBot.** The mainDialog in the skill has **completed**. " \ + "Sending EndOfConversation." + await context.send_activity(MessageFactory.text( + end_message_text, + input_hint=InputHints.ignoring_input + )) + + activity = Activity(type=ActivityTypes.end_of_conversation, value=result.result) + await context.send_activity(activity) + + # Save any state changes that might have happened during the turn. + await self._conversation_state.save_changes(context, False) + + + + def _get_active_dialog_context(self, dialog_context: DialogContext) -> DialogContext: + # Check non adaptive version of this method + raise NotImplementedError() From 365541a2c03d7cb946c2b2fb9bb7df26c42e1081 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 6 Mar 2020 12:16:44 -0800 Subject: [PATCH 11/38] Updates for dialog interruption and buffered response. Pending to move some classes to botbuilder.integration.aiohttp --- ci-pr-pipeline.yml | 1 + .../botbuilder/core/bot_adapter.py | 2 +- .../core/skills/skill_http_client.py | 6 +- .../tests/test_bot_framework_http_client.py | 4 +- .../botbuilder/dialogs/__init__.py | 2 + .../botbuilder/dialogs/dialog_events.py | 13 ++ .../botbuilder/dialogs/dialog_extensions.py | 82 +++++++ .../botbuilder/dialogs/skills/__init__.py | 4 +- .../skills/begin_skill_dialog_options.py | 18 ++ .../botbuilder/dialogs/skills/skill_dialog.py | 206 +++++++++++++----- .../dialogs/skills/skill_dialog_args.py | 11 - .../dialogs/skills/skill_dialog_options.py | 13 +- .../skills-buffered/parent/app.py | 2 +- .../skills-buffered/parent/bots/parent_bot.py | 2 +- .../dialog-to-dialog/dialog-root-bot/app.py | 2 +- .../dialog-root-bot/bots/root_bot.py | 2 +- .../dialog-root-bot/dialogs/main_dialog.py | 25 ++- .../simple-bot-to-bot/simple-root-bot/app.py | 2 +- .../simple-root-bot/bots/root_bot.py | 2 +- samples/experimental/test-protocol/app.py | 3 +- 20 files changed, 315 insertions(+), 87 deletions(-) create mode 100644 libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_events.py create mode 100644 libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py create mode 100644 libraries/botbuilder-dialogs/botbuilder/dialogs/skills/begin_skill_dialog_options.py delete mode 100644 libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_args.py diff --git a/ci-pr-pipeline.yml b/ci-pr-pipeline.yml index e70861290..9554439ba 100644 --- a/ci-pr-pipeline.yml +++ b/ci-pr-pipeline.yml @@ -53,6 +53,7 @@ jobs: pip install -e ./libraries/botbuilder-testing pip install -e ./libraries/botbuilder-integration-applicationinsights-aiohttp pip install -e ./libraries/botbuilder-adapters-slack + pip install -e ./libraries/botbuilder-integration-aiohttp pip install -r ./libraries/botframework-connector/tests/requirements.txt pip install -r ./libraries/botbuilder-core/tests/requirements.txt pip install coveralls diff --git a/libraries/botbuilder-core/botbuilder/core/bot_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_adapter.py index ca9a649bc..421e34ff2 100644 --- a/libraries/botbuilder-core/botbuilder/core/bot_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/bot_adapter.py @@ -14,7 +14,7 @@ class BotAdapter(ABC): BOT_IDENTITY_KEY = "BotIdentity" - BOT_OAUTH_SCOPE_KEY = "OAuthScope" + BOT_OAUTH_SCOPE_KEY = "botbuilder.core.BotAdapter.OAuthScope" BOT_CONNECTOR_CLIENT_KEY = "ConnectorClient" BOT_CALLBACK_HANDLER_KEY = "BotCallbackHandler" diff --git a/libraries/botbuilder-core/botbuilder/core/skills/skill_http_client.py b/libraries/botbuilder-core/botbuilder/core/skills/skill_http_client.py index 8699c0ad8..7f1fb6c19 100644 --- a/libraries/botbuilder-core/botbuilder/core/skills/skill_http_client.py +++ b/libraries/botbuilder-core/botbuilder/core/skills/skill_http_client.py @@ -1,10 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from botbuilder.core import ( - BotFrameworkHttpClient, - InvokeResponse, -) +from botbuilder.core import InvokeResponse +from botbuilder.integration.aiohttp import BotFrameworkHttpClient from botbuilder.core.skills import ( ConversationIdFactoryBase, SkillConversationIdFactoryOptions, diff --git a/libraries/botbuilder-core/tests/test_bot_framework_http_client.py b/libraries/botbuilder-core/tests/test_bot_framework_http_client.py index b2b5894d2..d6b4d33ae 100644 --- a/libraries/botbuilder-core/tests/test_bot_framework_http_client.py +++ b/libraries/botbuilder-core/tests/test_bot_framework_http_client.py @@ -1,7 +1,7 @@ import aiounittest -from botbuilder.core import BotFrameworkHttpClient - +from botbuilder.integration.aiohttp import BotFrameworkHttpClient +# TODO: move this to integration aiohttp class TestBotFrameworkHttpClient(aiounittest.AsyncTestCase): async def test_should_create_connector_client(self): with self.assertRaises(TypeError): diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py index d13238cca..2d0f4050b 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py @@ -8,6 +8,7 @@ from .about import __version__ from .component_dialog import ComponentDialog from .dialog_context import DialogContext +from .dialog_events import DialogEvents from .dialog_instance import DialogInstance from .dialog_reason import DialogReason from .dialog_set import DialogSet @@ -24,6 +25,7 @@ __all__ = [ "ComponentDialog", "DialogContext", + "DialogEvents", "DialogInstance", "DialogReason", "DialogSet", diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_events.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_events.py new file mode 100644 index 000000000..0c28a7e02 --- /dev/null +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_events.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from enum import Enum + + +class DialogEvents(str, Enum): + + begin_dialog = "beginDialog" + reprompt_dialog = "repromptDialog" + cancel_dialog = "cancelDialog" + activity_received = "activityReceived" + error = "error" diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py new file mode 100644 index 000000000..76d3f7ed1 --- /dev/null +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py @@ -0,0 +1,82 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core import BotAdapter, StatePropertyAccessor, TurnContext +from botbuilder.dialogs import ( + Dialog, + DialogContext, + DialogEvents, + DialogSet, + DialogTurnStatus, +) +from botbuilder.schema import Activity, ActivityTypes +from botframework.connector.auth import ClaimsIdentity, SkillValidation + + +class DialogExtensions: + @staticmethod + async def run_dialog( + dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor + ): + dialog_set = DialogSet(accessor) + dialog_set.add(dialog) + + dialog_context = await dialog_set.create_context(turn_context) + + claims = turn_context.turn_state.get(BotAdapter.BOT_IDENTITY_KEY) + if isinstance(claims, ClaimsIdentity) and SkillValidation.is_skill_claim( + claims.claims + ): + # The bot is running as a skill. + if ( + turn_context.activity.type == ActivityTypes.end_of_conversation + and dialog_context.stack + ): + await dialog_context.cancel_all_dialogs() + else: + # Process a reprompt event sent from the parent. + if ( + turn_context.activity.type == ActivityTypes.event + or turn_context.activity.name == DialogEvents.reprompt_dialog + and dialog_context.stack + ): + await dialog.reprompt_dialog() + return + + # Run the Dialog with the new message Activity and capture the results + # so we can send end of conversation if needed. + result = await dialog_context.continue_dialog() + if result.status == DialogTurnStatus.Empty: + start_message_text = f"Starting {dialog.id}" + await turn_context.send_trace_activity( + f"Extension {Dialog.__name__}.run_dialog", + label=start_message_text, + ) + result = await dialog_context.begin_dialog(dialog.id) + + # Send end of conversation if it is completed or cancelled. + if ( + result.status == DialogTurnStatus.Complete + or result.status == DialogTurnStatus.Cancelled + ): + end_message_text = f"Dialog {dialog.id} has **completed**. Sending EndOfConversation." + await turn_context.send_trace_activity( + f"Extension {Dialog.__name__}.run_dialog", + label=end_message_text, + value=result.result, + ) + + activity = Activity( + type=ActivityTypes.end_of_conversation, value=result.result + ) + await turn_context.send_activity(activity) + + else: + # The bot is running as a standard bot. + results = await dialog_context.continue_dialog() + if results.status == DialogTurnStatus.Empty: + await dialog_context.begin_dialog(dialog.id) + + @staticmethod + async def _get_active_dialog_context(dialog_context: DialogContext): + child = dialog_context.child diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/__init__.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/__init__.py index af1d0640b..9a804f378 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/__init__.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/__init__.py @@ -5,13 +5,13 @@ # license information. # -------------------------------------------------------------------------- -from .skill_dialog_args import SkillDialogArgs +from .begin_skill_dialog_options import BeginSkillDialogOptions from .skill_dialog_options import SkillDialogOptions from .skill_dialog import SkillDialog __all__ = [ - "SkillDialogArgs", + "BeginSkillDialogOptions", "SkillDialogOptions", "SkillDialog", ] diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/begin_skill_dialog_options.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/begin_skill_dialog_options.py new file mode 100644 index 000000000..baedd0311 --- /dev/null +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/begin_skill_dialog_options.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.schema import Activity + + +class BeginSkillDialogOptions: + def __init__(self, activity=Activity): + self.activity = Activity + + @staticmethod + def from_object(obj: object) -> "BeginSkillDialogOptions": + if isinstance(obj, dict) and "activity" in obj: + return BeginSkillDialogOptions(obj["activity"]) + if hasattr(obj, "activity"): + return BeginSkillDialogOptions(obj.activity) + + return None diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py index 710f79e4e..d51a68634 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -2,33 +2,35 @@ # Licensed under the MIT License. from copy import deepcopy - -from botbuilder.schema import Activity, ActivityTypes -from botbuilder.core import ConversationState, StatePropertyAccessor, TurnContext -from botbuilder.core.skills import BotFrameworkSkill - -from botbuilder.dialogs import Dialog, DialogContext - -from .skill_dialog_args import SkillDialogArgs +from typing import List + +from botbuilder.schema import Activity, ActivityTypes, DeliveryModes +from botbuilder.core import ( + BotAdapter, + TurnContext, +) +from botbuilder.core.skills import SkillConversationIdFactoryOptions + +from botbuilder.dialogs import ( + Dialog, + DialogContext, + DialogEvents, + DialogReason, + DialogInstance, +) + +from .begin_skill_dialog_options import BeginSkillDialogOptions from .skill_dialog_options import SkillDialogOptions class SkillDialog(Dialog): - def __init__( - self, dialog_options: SkillDialogOptions, conversation_state: ConversationState - ): - super().__init__(SkillDialog.__name__) + def __init__(self, dialog_options: SkillDialogOptions, dialog_id: str): + super().__init__(dialog_id) if not dialog_options: raise TypeError("SkillDialog.__init__(): dialog_options cannot be None.") - if not conversation_state: - raise TypeError( - "SkillDialog.__init__(): conversation_state cannot be None." - ) - self._dialog_options = dialog_options - self._conversation_state = conversation_state - self._active_skill_property: StatePropertyAccessor = conversation_state.create_property( - f"{SkillDialog.__module__}.{SkillDialog.__name__}.ActiveSkillProperty" - ) + + self.dialog_options = dialog_options + self._DELIVER_MODE_STATE_KEY = "deliverymode" async def begin_dialog(self, dialog_context: DialogContext, options: object = None): """ @@ -36,7 +38,7 @@ async def begin_dialog(self, dialog_context: DialogContext, options: object = No :param dialog_context: The dialog context for the current turn of conversation. :param options: (Optional) additional argument(s) to pass to the dialog being started. """ - dialog_args = SkillDialog._validate_begin_dialog_options(options) + dialog_args = SkillDialog._validate_begin_dialog_args(options) await dialog_context.context.trace_activity( f"{SkillDialog.__name__}.BeginDialogAsync()", @@ -48,61 +50,153 @@ async def begin_dialog(self, dialog_context: DialogContext, options: object = No # Apply conversation reference and common properties from incoming activity before sending. TurnContext.apply_conversation_reference( + skill_activity, TurnContext.get_conversation_reference(dialog_context.context.activity), - True, + is_incoming=True, ) + dialog_context.active_dialog.state[ + self._DELIVER_MODE_STATE_KEY + ] = dialog_args.activity.delivery_mode + # Send the activity to the skill. await self._send_to_skill(dialog_context, skill_activity, dialog_args.Skill) return Dialog.end_of_turn + async def continue_dialog(self, dialog_context: DialogContext): + await dialog_context.context.send_trace_activity( + f"{SkillDialog.__name__}.continue_dialog()", + label=f"ActivityType: {dialog_context.context.activity.type}", + ) + + # Handle EndOfConversation from the skill (this will be sent to the this dialog by the SkillHandler if + # received from the Skill) + if dialog_context.context.activity.type == ActivityTypes.end_of_conversation: + await dialog_context.context.send_trace_activity( + f"{SkillDialog.__name__}.continue_dialog()", + label=f"Got {ActivityTypes.end_of_conversation}", + ) + + return await dialog_context.end_dialog( + dialog_context.context.activity.value + ) + + # Forward only Message and Event activities to the skill + if ( + dialog_context.context.activity.type == ActivityTypes.message + or dialog_context.context.activity.type == ActivityTypes.event + ): + # Create deep clone of the original activity to avoid altering it before forwarding it. + skill_activity = deepcopy(dialog_context.context.activity) + skill_activity.delivery_mode = dialog_context.active_dialog.state[ + self._DELIVER_MODE_STATE_KEY + ] + + # Just forward to the remote skill + eoc_activity = await self._send_to_skill( + dialog_context.context, skill_activity + ) + if eoc_activity: + return await dialog_context.end_dialog(eoc_activity.value) + + return self.end_of_turn + + async def reprompt_dialog( # pylint: disable=unused-argument + self, context: TurnContext, instance: DialogInstance + ): + # Create and send an event to the skill so it can resume the dialog. + reprompt_event = Activity( + type=ActivityTypes.event, name=DialogEvents.reprompt_dialog + ) + + # Apply conversation reference and common properties from incoming activity before sending. + TurnContext.apply_conversation_reference( + reprompt_event, + TurnContext.get_conversation_reference(context.activity), + is_incoming=True, + ) + + await self._send_to_skill(context, reprompt_event) + + async def resume_dialog( # pylint: disable=unused-argument + self, dialog_context: "DialogContext", reason: DialogReason, result: object + ): + await self.reprompt_dialog(dialog_context.context, dialog_context.active_dialog) + return self.end_of_turn + + async def end_dialog( + self, context: TurnContext, instance: DialogInstance, reason: DialogReason + ): + # Send of of conversation to the skill if the dialog has been cancelled. + if reason == DialogReason.CancelCalled or reason == DialogReason.ReplaceCalled: + await context.send_trace_activity( + f"{SkillDialog.__name__}.end_dialog()", + label=f"ActivityType: {context.activity.type}", + ) + activity = Activity(type=ActivityTypes.end_of_conversation) + + # Apply conversation reference and common properties from incoming activity before sending. + TurnContext.apply_conversation_reference( + activity, + TurnContext.get_conversation_reference(context.activity), + is_incoming=True, + ) + activity.channel_data = context.activity.channel_data + activity.additional_properties = context.activity.additional_properties + + await self._send_to_skill(context, activity) + + await super().end_dialog(context, instance, reason) + @staticmethod - def _validate_begin_dialog_options(options: object) -> SkillDialogArgs: + def _validate_begin_dialog_args(options: object) -> BeginSkillDialogOptions: if not options: raise TypeError("options cannot be None.") - if isinstance(options, dict) and "skill" in options and "activity" in options: - skill_args = SkillDialogArgs( - skill=options["skill"], activity=options["skill"] - ) - elif hasattr(options, "skill") and hasattr(options, "activity"): - skill_args = SkillDialogArgs( - skill=options["skill"], activity=options["skill"] + dialog_args = BeginSkillDialogOptions.from_object(options) + + if not dialog_args: + raise TypeError( + "SkillDialog: options object not valid as BeginSkillDialogOptions." ) - else: - raise TypeError("SkillDialog: options object not valid as SkillDialogArgs.") - if not skill_args.activity: + if not dialog_args.activity: raise TypeError( - "SkillDialog: activity object in options as SkillDialogArgs cannot be None." + "SkillDialog: activity object in options as BeginSkillDialogOptions cannot be None." ) # Only accept Message or Event activities if ( - skill_args.activity.type != ActivityTypes.message - and skill_args.activity.type != ActivityTypes.event + dialog_args.activity.type != ActivityTypes.message + and dialog_args.activity.type != ActivityTypes.event ): raise TypeError( f"Only {ActivityTypes.message} and {ActivityTypes.event} activities are supported." - f" Received activity of type {skill_args.activity.type}." + f" Received activity of type {dialog_args.activity.type}." ) - return skill_args + return dialog_args async def _send_to_skill( - self, - dialog_context: DialogContext, - activity: Activity, - skill_info: BotFrameworkSkill, - ): + self, context: TurnContext, activity: Activity, + ) -> Activity: + # Create a conversationId to interact with the skill and send the activity + conversation_id_factory_options = SkillConversationIdFactoryOptions( + from_bot_oauth_scope=context.turn_state.get(BotAdapter.BOT_OAUTH_SCOPE_KEY), + from_bot_id=self.dialog_options.bot_id, + activity=activity, + bot_framework_skill=self.dialog_options.skill, + ) + + skill_conversation_id = await self.dialog_options.conversation_id_factory.create_skill_conversation_id( + conversation_id_factory_options + ) + # Always save state before forwarding # (the dialog stack won't get updated with the skillDialog and things won't work if you don't) - await self._conversation_state.save_changes(dialog_context.context, True) + skill_info = self.dialog_options.skill + await self.dialog_options.conversation_state.save_changes(context, True) - # Create a conversation_id to interact with the skill and send the activity - skill_conversation_id = await self._dialog_options.conversation_id_factory.create_skill_conversation_id( - TurnContext.get_conversation_reference(activity) - ) response = await self._dialog_options.skill_client.post_activity( self._dialog_options.bot_id, skill_info.app_id, @@ -118,3 +212,17 @@ async def _send_to_skill( f'Error invoking the skill id: "{skill_info.id}" at "{skill_info.skill_endpoint}"' f" (status is {response.status}). \r\n {response.body}" ) + + eoc_activity: Activity = None + if activity.delivery_mode == DeliveryModes.buffered_replies and response.body: + # Process replies in the response.Body. + response.body: List[Activity] + for from_skill_activity in response.body: + if from_skill_activity.type == ActivityTypes.end_of_conversation: + # Capture the EndOfConversation activity if it was sent from skill + eoc_activity = from_skill_activity + else: + # Send the response back to the channel. + await context.send_activity(from_skill_activity) + + return eoc_activity diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_args.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_args.py deleted file mode 100644 index 0e127053a..000000000 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_args.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.schema import Activity -from botbuilder.core.skills import BotFrameworkSkill - - -class SkillDialogArgs: - def __init__(self, skill: BotFrameworkSkill = None, activity: Activity = None): - self.skill = skill - self.activity = activity diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_options.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_options.py index 369ead801..53d56f72e 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_options.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_options.py @@ -1,7 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from botbuilder.core.skills import BotFrameworkClient, SkillConversationIdFactory +from botbuilder.core import ConversationState +from botbuilder.core.skills import ( + BotFrameworkClient, + BotFrameworkSkill, + ConversationIdFactoryBase, +) class SkillDialogOptions: @@ -10,9 +15,13 @@ def __init__( bot_id: str = None, skill_client: BotFrameworkClient = None, skill_host_endpoint: str = None, - conversation_id_factory: SkillConversationIdFactory = None, + skill: BotFrameworkSkill = None, + conversation_id_factory: ConversationIdFactoryBase = None, + conversation_state: ConversationState = None, ): self.bot_id = bot_id self.skill_client = skill_client self.skill_host_endpoint = skill_host_endpoint + self.skill = skill self.conversation_id_factory = conversation_id_factory + self.conversation_state = conversation_state diff --git a/samples/experimental/skills-buffered/parent/app.py b/samples/experimental/skills-buffered/parent/app.py index 585a6873f..d1e9fbc0a 100644 --- a/samples/experimental/skills-buffered/parent/app.py +++ b/samples/experimental/skills-buffered/parent/app.py @@ -12,11 +12,11 @@ MemoryStorage, TurnContext, BotFrameworkAdapter, - BotFrameworkHttpClient, ) from botbuilder.core.integration import ( aiohttp_channel_service_routes, aiohttp_error_middleware, + BotFrameworkHttpClient ) from botbuilder.core.skills import SkillHandler from botbuilder.schema import Activity diff --git a/samples/experimental/skills-buffered/parent/bots/parent_bot.py b/samples/experimental/skills-buffered/parent/bots/parent_bot.py index 91b85b654..19748a4e8 100644 --- a/samples/experimental/skills-buffered/parent/bots/parent_bot.py +++ b/samples/experimental/skills-buffered/parent/bots/parent_bot.py @@ -6,9 +6,9 @@ from botbuilder.core import ( ActivityHandler, TurnContext, - BotFrameworkHttpClient, MessageFactory, ) +from botbuilder.integration import BotFrameworkHttpClient from botbuilder.schema import DeliveryModes diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py index eb99b1c11..3a2e5193f 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py @@ -5,7 +5,6 @@ from aiohttp.web import Request, Response from botbuilder.core import ( BotFrameworkAdapterSettings, - BotFrameworkHttpClient, ConversationState, MemoryStorage, TurnContext, @@ -15,6 +14,7 @@ from botbuilder.core.integration import ( aiohttp_channel_service_routes, aiohttp_error_middleware, + BotFrameworkHttpClient ) from botbuilder.core.skills import SkillConversationIdFactory, SkillHandler from botbuilder.schema import Activity, ActivityTypes diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py index 0561714ba..23ab7f49a 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py @@ -5,12 +5,12 @@ from botbuilder.core import ( ActivityHandler, - BotFrameworkHttpClient, ConversationState, MessageFactory, TurnContext, ) from botbuilder.core.skills import SkillConversationIdFactory +from botbuilder.integration.aiohttp import BotFrameworkHttpClient from botbuilder.dialogs import Dialog from botbuilder.schema import ActivityTypes, Attachment, ChannelAccount diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py index 6877aa141..e86f63258 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py @@ -23,6 +23,9 @@ class MainDialog(ComponentDialog): + + ACTIVE_SKILL_PROPERTY_NAME = f"MainDialog.ActiveSkillProperty" + def __init__( self, conversation_state: ConversationState, @@ -54,15 +57,19 @@ def __init__( # ChoicePrompt to render available skills and skill actions self.add_dialog(ChoicePrompt(ChoicePrompt.__name__)) - # SkillDialog used to wrap interaction with the selected skill - skill_dialog_options = SkillDialogOptions( - bot_id=bot_id, - conversation_id_factory=conversation_id_factory, - skill_client=skill_client, - skill_host_endpoint=skills_config.SKILL_HOST_ENDPOINT, - ) + # Create SkillDialog instances for the configured skills + for _, skill_info in skills_config.SKILLS.items(): + # SkillDialog used to wrap interaction with the selected skill + skill_dialog_options = SkillDialogOptions( + bot_id=bot_id, + conversation_id_factory=conversation_id_factory, + skill_client=skill_client, + skill=skill_info, + skill_host_endpoint=skills_config.SKILL_HOST_ENDPOINT, + conversation_state=conversation_state + ) - self.add_dialog(SkillDialog(skill_dialog_options, conversation_state)) + self.add_dialog(SkillDialog(skill_dialog_options, skill_info.id)) # Main waterfall dialog for this bot self.add_dialog( @@ -72,6 +79,8 @@ def __init__( ) ) + self._active_skill_property = conversation_state.create_property() + self.initial_dialog_id = WaterfallDialog.__name__ async def select_skill_step( diff --git a/samples/experimental/skills-prototypes/simple-bot-to-bot/simple-root-bot/app.py b/samples/experimental/skills-prototypes/simple-bot-to-bot/simple-root-bot/app.py index d3c0aafd1..2915c0d47 100644 --- a/samples/experimental/skills-prototypes/simple-bot-to-bot/simple-root-bot/app.py +++ b/samples/experimental/skills-prototypes/simple-bot-to-bot/simple-root-bot/app.py @@ -9,7 +9,6 @@ from aiohttp.web import Request, Response from botbuilder.core import ( BotFrameworkAdapterSettings, - BotFrameworkHttpClient, ConversationState, MemoryStorage, TurnContext, @@ -18,6 +17,7 @@ from botbuilder.core.integration import ( aiohttp_channel_service_routes, aiohttp_error_middleware, + BotFrameworkHttpClient, ) from botbuilder.core.skills import SkillConversationIdFactory, SkillHandler from botbuilder.schema import Activity, ActivityTypes diff --git a/samples/experimental/skills-prototypes/simple-bot-to-bot/simple-root-bot/bots/root_bot.py b/samples/experimental/skills-prototypes/simple-bot-to-bot/simple-root-bot/bots/root_bot.py index 78ca44ed4..c271904fd 100644 --- a/samples/experimental/skills-prototypes/simple-bot-to-bot/simple-root-bot/bots/root_bot.py +++ b/samples/experimental/skills-prototypes/simple-bot-to-bot/simple-root-bot/bots/root_bot.py @@ -2,12 +2,12 @@ from botbuilder.core import ( ActivityHandler, - BotFrameworkHttpClient, ConversationState, MessageFactory, TurnContext, ) from botbuilder.core.skills import SkillConversationIdFactory +from botbuilder.integration.aiohttp import BotFrameworkHttpClient from botbuilder.schema import ActivityTypes, ChannelAccount diff --git a/samples/experimental/test-protocol/app.py b/samples/experimental/test-protocol/app.py index ed7625cbc..e890718e7 100644 --- a/samples/experimental/test-protocol/app.py +++ b/samples/experimental/test-protocol/app.py @@ -5,8 +5,7 @@ from aiohttp.web import Request, Response from botframework.connector.auth import AuthenticationConfiguration, SimpleCredentialProvider -from botbuilder.core import BotFrameworkHttpClient -from botbuilder.core.integration import aiohttp_channel_service_routes +from botbuilder.core.integration import aiohttp_channel_service_routes, BotFrameworkHttpClient from botbuilder.schema import Activity from config import DefaultConfig From 1a921fdba3ea1d6df3d217346770ecc5de477c53 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 6 Mar 2020 12:22:35 -0800 Subject: [PATCH 12/38] Relates to in post_activity in BotFrameworkHttpClient --- .../botbuilder/integration/aiohttp/bot_framework_http_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py index 214432062..a5cf82267 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py @@ -69,6 +69,7 @@ async def post_activity( original_conversation_id = activity.conversation.id original_service_url = activity.service_url original_caller_id = activity.caller_id + original_relates_to = activity.relates_to try: activity.conversation.id = conversation_id @@ -99,6 +100,7 @@ async def post_activity( activity.conversation.id = original_conversation_id activity.service_url = original_service_url activity.caller_id = original_caller_id + activity.relates_to = original_relates_to async def post_buffered_activity( self, From a5670c1c677ba294f9af2919605e1880e51d40be Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 6 Mar 2020 12:39:19 -0800 Subject: [PATCH 13/38] fix on BeginSkillDialogOptions --- .../botbuilder/dialogs/skills/begin_skill_dialog_options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/begin_skill_dialog_options.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/begin_skill_dialog_options.py index baedd0311..62a02ab2e 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/begin_skill_dialog_options.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/begin_skill_dialog_options.py @@ -5,8 +5,8 @@ class BeginSkillDialogOptions: - def __init__(self, activity=Activity): - self.activity = Activity + def __init__(self, activity: Activity): # pylint: disable=unused-argument + self.activity = activity @staticmethod def from_object(obj: object) -> "BeginSkillDialogOptions": From 1561e2dc1b6db39b7a22ff3ac4ddf33d288ae1e1 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 6 Mar 2020 14:18:35 -0800 Subject: [PATCH 14/38] Moved SkillHttpClient to correct library with corresponding tests. Fix SkillDialog. --- .../core/skills/skill_http_client.py | 72 ------------------- .../botbuilder/dialogs/dialog_extensions.py | 4 -- .../botbuilder/dialogs/skills/skill_dialog.py | 25 ++++--- .../aiohttp/skills/skill_http_client.py | 61 ++++++++++++---- .../tests/test_bot_framework_http_client.py | 2 +- 5 files changed, 62 insertions(+), 102 deletions(-) delete mode 100644 libraries/botbuilder-core/botbuilder/core/skills/skill_http_client.py rename libraries/{botbuilder-core => botbuilder-integration-aiohttp}/tests/test_bot_framework_http_client.py (87%) diff --git a/libraries/botbuilder-core/botbuilder/core/skills/skill_http_client.py b/libraries/botbuilder-core/botbuilder/core/skills/skill_http_client.py deleted file mode 100644 index 7f1fb6c19..000000000 --- a/libraries/botbuilder-core/botbuilder/core/skills/skill_http_client.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import InvokeResponse -from botbuilder.integration.aiohttp import BotFrameworkHttpClient -from botbuilder.core.skills import ( - ConversationIdFactoryBase, - SkillConversationIdFactoryOptions, - BotFrameworkSkill, -) -from botbuilder.schema import Activity -from botframework.connector.auth import ( - AuthenticationConstants, - ChannelProvider, - GovernmentConstants, - SimpleCredentialProvider, -) - - -class SkillHttpClient(BotFrameworkHttpClient): - def __init__( - self, - credential_provider: SimpleCredentialProvider, - skill_conversation_id_factory: ConversationIdFactoryBase, - channel_provider: ChannelProvider = None, - ): - if not skill_conversation_id_factory: - raise TypeError( - "SkillHttpClient(): skill_conversation_id_factory can't be None" - ) - - super().__init__(credential_provider) - - self._skill_conversation_id_factory = skill_conversation_id_factory - self._channel_provider = channel_provider - - async def post_activity_to_skill( - self, - from_bot_id: str, - to_skill: BotFrameworkSkill, - service_url: str, - activity: Activity, - originating_audience: str = None, - ) -> InvokeResponse: - - if originating_audience is None: - originating_audience = ( - GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE - if self._channel_provider is not None - and self._channel_provider.IsGovernment() - else AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE - ) - - options = SkillConversationIdFactoryOptions( - from_bot_oauth_scope=originating_audience, - from_bot_id=from_bot_id, - activity=activity, - bot_framework_skill=to_skill, - ) - - skill_conversation_id = await self._skill_conversation_id_factory.create_skill_conversation_id( - options - ) - - return await super().post_activity( - from_bot_id, - to_skill.app_id, - to_skill.skill_endpoint, - service_url, - skill_conversation_id, - activity, - ) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py index 76d3f7ed1..6ff2e9460 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py @@ -76,7 +76,3 @@ async def run_dialog( results = await dialog_context.continue_dialog() if results.status == DialogTurnStatus.Empty: await dialog_context.begin_dialog(dialog.id) - - @staticmethod - async def _get_active_dialog_context(dialog_context: DialogContext): - child = dialog_context.child diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py index d51a68634..d3ab3ffa6 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -19,8 +19,8 @@ DialogInstance, ) -from .begin_skill_dialog_options import BeginSkillDialogOptions -from .skill_dialog_options import SkillDialogOptions +from .begin_skilldialog_options import BeginSkillDialogOptions +from .skilldialog_options import SkillDialogOptions class SkillDialog(Dialog): @@ -30,7 +30,7 @@ def __init__(self, dialog_options: SkillDialogOptions, dialog_id: str): raise TypeError("SkillDialog.__init__(): dialog_options cannot be None.") self.dialog_options = dialog_options - self._DELIVER_MODE_STATE_KEY = "deliverymode" + self._deliver_mode_state_key = "deliverymode" async def begin_dialog(self, dialog_context: DialogContext, options: object = None): """ @@ -56,12 +56,15 @@ async def begin_dialog(self, dialog_context: DialogContext, options: object = No ) dialog_context.active_dialog.state[ - self._DELIVER_MODE_STATE_KEY + self._deliver_mode_state_key ] = dialog_args.activity.delivery_mode # Send the activity to the skill. - await self._send_to_skill(dialog_context, skill_activity, dialog_args.Skill) - return Dialog.end_of_turn + eoc_activity = await self._send_to_skill(dialog_context.context, skill_activity) + if not eoc_activity: + return await dialog_context.end_dialog(eoc_activity.value) + + return self.end_of_turn async def continue_dialog(self, dialog_context: DialogContext): await dialog_context.context.send_trace_activity( @@ -89,7 +92,7 @@ async def continue_dialog(self, dialog_context: DialogContext): # Create deep clone of the original activity to avoid altering it before forwarding it. skill_activity = deepcopy(dialog_context.context.activity) skill_activity.delivery_mode = dialog_context.active_dialog.state[ - self._DELIVER_MODE_STATE_KEY + self._deliver_mode_state_key ] # Just forward to the remote skill @@ -128,7 +131,7 @@ async def end_dialog( self, context: TurnContext, instance: DialogInstance, reason: DialogReason ): # Send of of conversation to the skill if the dialog has been cancelled. - if reason == DialogReason.CancelCalled or reason == DialogReason.ReplaceCalled: + if reason in (DialogReason.CancelCalled, DialogReason.ReplaceCalled): await context.send_trace_activity( f"{SkillDialog.__name__}.end_dialog()", label=f"ActivityType: {context.activity.type}", @@ -197,11 +200,11 @@ async def _send_to_skill( skill_info = self.dialog_options.skill await self.dialog_options.conversation_state.save_changes(context, True) - response = await self._dialog_options.skill_client.post_activity( - self._dialog_options.bot_id, + response = await self.dialog_options.skill_client.post_activity( + self.dialog_options.bot_id, skill_info.app_id, skill_info.skill_endpoint, - self._dialog_options.skill_host_endpoint, + self.dialog_options.skill_host_endpoint, skill_conversation_id, activity, ) diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py index 6f6c4910c..df875f734 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py @@ -1,42 +1,75 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from logging import Logger +from botbuilder.core import InvokeResponse +from botbuilder.integration.aiohttp import BotFrameworkHttpClient +from botbuilder.core.skills import ( + ConversationIdFactoryBase, + SkillConversationIdFactoryOptions, + BotFrameworkSkill, +) +from botbuilder.schema import Activity from botframework.connector.auth import ( + AuthenticationConstants, ChannelProvider, - CredentialProvider, + GovernmentConstants, + SimpleCredentialProvider, ) -from botbuilder.core import InvokeResponse, TurnContext -from botbuilder.core.skills import BotFrameworkSkill, SkillConversationIdFactory -from botbuilder.integration.aiohttp import BotFrameworkHttpClient -from botbuilder.schema import Activity class SkillHttpClient(BotFrameworkHttpClient): def __init__( self, - credential_provider: CredentialProvider, - conversation_id_factory: SkillConversationIdFactory, + credential_provider: SimpleCredentialProvider, + skill_conversation_id_factory: ConversationIdFactoryBase, channel_provider: ChannelProvider = None, logger: Logger = None, ): - super().__init__(credential_provider, channel_provider, logger) - self._conversation_id_factory = conversation_id_factory + if not skill_conversation_id_factory: + raise TypeError( + "SkillHttpClient(): skill_conversation_id_factory can't be None" + ) - async def post_activity( + super().__init__(credential_provider) + + self._skill_conversation_id_factory = skill_conversation_id_factory + self._channel_provider = channel_provider + + async def post_activity_to_skill( self, from_bot_id: str, to_skill: BotFrameworkSkill, - callback_url: str, + service_url: str, activity: Activity, + originating_audience: str = None, ) -> InvokeResponse: - skill_conversation_id = await self._conversation_id_factory.create_skill_conversation_id( - TurnContext.get_conversation_reference(activity) + + if originating_audience is None: + originating_audience = ( + GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + if self._channel_provider is not None + and self._channel_provider.IsGovernment() + else AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + ) + + options = SkillConversationIdFactoryOptions( + from_bot_oauth_scope=originating_audience, + from_bot_id=from_bot_id, + activity=activity, + bot_framework_skill=to_skill, + ) + + skill_conversation_id = await self._skill_conversation_id_factory.create_skill_conversation_id( + options ) return await super().post_activity( from_bot_id, to_skill.app_id, to_skill.skill_endpoint, - callback_url, + service_url, skill_conversation_id, activity, ) diff --git a/libraries/botbuilder-core/tests/test_bot_framework_http_client.py b/libraries/botbuilder-integration-aiohttp/tests/test_bot_framework_http_client.py similarity index 87% rename from libraries/botbuilder-core/tests/test_bot_framework_http_client.py rename to libraries/botbuilder-integration-aiohttp/tests/test_bot_framework_http_client.py index d6b4d33ae..7e01f390b 100644 --- a/libraries/botbuilder-core/tests/test_bot_framework_http_client.py +++ b/libraries/botbuilder-integration-aiohttp/tests/test_bot_framework_http_client.py @@ -1,7 +1,7 @@ import aiounittest from botbuilder.integration.aiohttp import BotFrameworkHttpClient -# TODO: move this to integration aiohttp + class TestBotFrameworkHttpClient(aiounittest.AsyncTestCase): async def test_should_create_connector_client(self): with self.assertRaises(TypeError): From a00b663e319c3f1291c26ca0c53ac7f5238bd98c Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 6 Mar 2020 14:19:03 -0800 Subject: [PATCH 15/38] black: Moved SkillHttpClient to correct library with corresponding tests. Fix SkillDialog. --- .../botbuilder/dialogs/skills/skill_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py index d3ab3ffa6..d1c2a3855 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -63,7 +63,7 @@ async def begin_dialog(self, dialog_context: DialogContext, options: object = No eoc_activity = await self._send_to_skill(dialog_context.context, skill_activity) if not eoc_activity: return await dialog_context.end_dialog(eoc_activity.value) - + return self.end_of_turn async def continue_dialog(self, dialog_context: DialogContext): From fe2ab2c017486572c1a25b80fb91a6f59fd043ef Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 6 Mar 2020 14:25:04 -0800 Subject: [PATCH 16/38] relative import fix --- .../botbuilder/dialogs/skills/skill_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py index d1c2a3855..e86c5cd2f 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -19,8 +19,8 @@ DialogInstance, ) -from .begin_skilldialog_options import BeginSkillDialogOptions -from .skilldialog_options import SkillDialogOptions +from .begin_skill_dialog_options import BeginSkillDialogOptions +from .skill_dialog_options import SkillDialogOptions class SkillDialog(Dialog): From 7f9943409a088e6a855139e6bcb2ca2ead31db1e Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 6 Mar 2020 14:48:51 -0800 Subject: [PATCH 17/38] Removed unused import --- .../botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py index 6ff2e9460..96d8f1bdb 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py @@ -4,7 +4,6 @@ from botbuilder.core import BotAdapter, StatePropertyAccessor, TurnContext from botbuilder.dialogs import ( Dialog, - DialogContext, DialogEvents, DialogSet, DialogTurnStatus, From 4703510b6670f1c0f74b236f8e5bcb72b5106d9c Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Fri, 6 Mar 2020 17:11:22 -0600 Subject: [PATCH 18/38] Modified TurnContext.send_trace_activity to default args. --- libraries/botbuilder-core/botbuilder/core/turn_context.py | 2 +- .../botbuilder/dialogs/skills/skill_dialog.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/turn_context.py b/libraries/botbuilder-core/botbuilder/core/turn_context.py index b3ec326c8..b33c3a399 100644 --- a/libraries/botbuilder-core/botbuilder/core/turn_context.py +++ b/libraries/botbuilder-core/botbuilder/core/turn_context.py @@ -295,7 +295,7 @@ async def next_handler(): return await logic async def send_trace_activity( - self, name: str, value: object, value_type: str, label: str + self, name: str, value: object = None, value_type: str = None, label: str = None ) -> ResourceResponse: trace_activity = Activity( type=ActivityTypes.trace, diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py index e86c5cd2f..e0243a29a 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -40,7 +40,7 @@ async def begin_dialog(self, dialog_context: DialogContext, options: object = No """ dialog_args = SkillDialog._validate_begin_dialog_args(options) - await dialog_context.context.trace_activity( + await dialog_context.context.send_trace_activity( f"{SkillDialog.__name__}.BeginDialogAsync()", label=f"Using activity of type: {dialog_args.activity.type}", ) From 7aabfe4eac46ef2b052daec4c5b22b4c9ccd89e7 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Fri, 6 Mar 2020 17:50:59 -0600 Subject: [PATCH 19/38] Removed argument checks that didn't exist in C#. Fixed bug in SkillDialog.begin_dialog --- .../skill_conversation_id_factory_options.py | 20 ------------------- .../skills/skill_conversation_reference.py | 8 -------- .../botbuilder/dialogs/skills/skill_dialog.py | 2 +- 3 files changed, 1 insertion(+), 29 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_id_factory_options.py b/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_id_factory_options.py index 9eae6ec75..43d19c600 100644 --- a/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_id_factory_options.py +++ b/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_id_factory_options.py @@ -13,26 +13,6 @@ def __init__( activity: Activity, bot_framework_skill: BotFrameworkSkill, ): - if from_bot_oauth_scope is None: - raise TypeError( - "SkillConversationIdFactoryOptions(): from_bot_oauth_scope cannot be None." - ) - - if from_bot_id is None: - raise TypeError( - "SkillConversationIdFactoryOptions(): from_bot_id cannot be None." - ) - - if activity is None: - raise TypeError( - "SkillConversationIdFactoryOptions(): activity cannot be None." - ) - - if bot_framework_skill is None: - raise TypeError( - "SkillConversationIdFactoryOptions(): bot_framework_skill cannot be None." - ) - self.from_bot_oauth_scope = from_bot_oauth_scope self.from_bot_id = from_bot_id self.activity = activity diff --git a/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_reference.py b/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_reference.py index 877f83141..341fb8104 100644 --- a/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_reference.py +++ b/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_reference.py @@ -9,13 +9,5 @@ class SkillConversationReference: """ def __init__(self, conversation_reference: ConversationReference, oauth_scope: str): - if conversation_reference is None: - raise TypeError( - "SkillConversationReference(): conversation_reference cannot be None." - ) - - if oauth_scope is None: - raise TypeError("SkillConversationReference(): oauth_scope cannot be None.") - self.conversation_reference = conversation_reference self.oauth_scope = oauth_scope diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py index e0243a29a..0d30d39d9 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -61,7 +61,7 @@ async def begin_dialog(self, dialog_context: DialogContext, options: object = No # Send the activity to the skill. eoc_activity = await self._send_to_skill(dialog_context.context, skill_activity) - if not eoc_activity: + if eoc_activity: return await dialog_context.end_dialog(eoc_activity.value) return self.end_of_turn From f9976a1f529f3b392004d8e56b7886b6040dbeb0 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Sat, 7 Mar 2020 05:42:53 -0600 Subject: [PATCH 20/38] Added initial SkillDialog unit test. --- .../botbuilder/core/adapters/test_adapter.py | 4 +- .../tests/test_skill_dialog.py | 199 ++++++++++++++++++ 2 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 libraries/botbuilder-dialogs/tests/test_skill_dialog.py diff --git a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py index 7df9c2506..d350ee3f3 100644 --- a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py @@ -205,7 +205,9 @@ async def receive_activity(self, activity): return await self.run_pipeline(context, self.logic) def get_next_activity(self) -> Activity: - return self.activity_buffer.pop(0) + if len(self.activity_buffer) > 0: + return self.activity_buffer.pop(0) + return None async def send(self, user_says) -> object: """ diff --git a/libraries/botbuilder-dialogs/tests/test_skill_dialog.py b/libraries/botbuilder-dialogs/tests/test_skill_dialog.py new file mode 100644 index 000000000..9f9dfb5bb --- /dev/null +++ b/libraries/botbuilder-dialogs/tests/test_skill_dialog.py @@ -0,0 +1,199 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import uuid +from typing import Callable, Union +from unittest.mock import Mock + +import aiounittest +from botbuilder.core import ( + ConversationState, + MemoryStorage, + InvokeResponse, + TurnContext, + MessageFactory, +) +from botbuilder.core.skills import ( + BotFrameworkSkill, + ConversationIdFactoryBase, + SkillConversationIdFactoryOptions, + SkillConversationReference, + BotFrameworkClient, +) +from botbuilder.schema import Activity, ActivityTypes, ConversationReference +from botbuilder.testing import DialogTestClient + +from botbuilder.dialogs import ( + SkillDialog, + SkillDialogOptions, + BeginSkillDialogOptions, + DialogTurnStatus, +) + + +class SimpleConversationIdFactory(ConversationIdFactoryBase): + def __init__(self): + self.conversation_refs = {} + + async def create_skill_conversation_id( + self, + options_or_conversation_reference: Union[ + SkillConversationIdFactoryOptions, ConversationReference + ], + ) -> str: + key = ( + options_or_conversation_reference.activity.conversation.id + + options_or_conversation_reference.activity.service_url + ) + if key not in self.conversation_refs: + self.conversation_refs[key] = SkillConversationReference( + conversation_reference=TurnContext.get_conversation_reference( + options_or_conversation_reference.activity + ), + oauth_scope=options_or_conversation_reference.from_bot_oauth_scope, + ) + return key + + async def get_conversation_reference( + self, skill_conversation_id: str + ) -> Union[SkillConversationReference, ConversationReference]: + return self.conversation_refs[skill_conversation_id] + + async def delete_conversation_reference(self, skill_conversation_id: str): + raise NotImplementedError() + + +class SkillDialogTests(aiounittest.AsyncTestCase): + async def test_constructor_validation_test(self): + # missing dialog_id + with self.assertRaises(TypeError): + SkillDialog(SkillDialogOptions(), None) + + # missing dialog options + with self.assertRaises(TypeError): + SkillDialog(None, "dialog_id") + + async def test_begin_dialog_options_validation(self): + dialog_options = SkillDialogOptions() + sut = SkillDialog(dialog_options, dialog_id="dialog_id") + + # empty options should raise + client = DialogTestClient("test", sut) + with self.assertRaises(TypeError): + await client.send_activity("irrelevant") + + # non DialogArgs should raise + client = DialogTestClient("test", sut, {}) + with self.assertRaises(TypeError): + await client.send_activity("irrelevant") + + # Activity in DialogArgs should be set + client = DialogTestClient("test", sut, BeginSkillDialogOptions(None)) + with self.assertRaises(TypeError): + await client.send_activity("irrelevant") + + # Only Message and Event activities are supported + client = DialogTestClient( + "test", + sut, + BeginSkillDialogOptions(Activity(type=ActivityTypes.conversation_update)), + ) + with self.assertRaises(TypeError): + await client.send_activity("irrelevant") + + async def test_begin_dialog_calls_skill(self): + activity_sent = None + from_bot_id_sent = None + to_bot_id_sent = None + to_url_sent = None + + async def capture( + from_bot_id: str, + to_bot_id: str, + to_url: str, + service_url: str, # pylint: disable=unused-argument + conversation_id: str, # pylint: disable=unused-argument + activity: Activity, + ): + nonlocal from_bot_id_sent, to_bot_id_sent, to_url_sent, activity_sent + from_bot_id_sent = from_bot_id + to_bot_id_sent = to_bot_id + to_url_sent = to_url + activity_sent = activity + + mock_skill_client = self._create_mock_skill_client(capture) + + conversation_state = ConversationState(MemoryStorage()) + dialog_options = self._create_skill_dialog_options( + conversation_state, mock_skill_client + ) + + sut = SkillDialog(dialog_options, "dialog_id") + activity_to_send = MessageFactory.text(str(uuid.uuid4())) + + client = DialogTestClient( + "test", + sut, + BeginSkillDialogOptions(activity=activity_to_send), + conversation_state=conversation_state, + ) + + await client.send_activity(MessageFactory.text("irrelevant")) + + assert dialog_options.bot_id == from_bot_id_sent + assert dialog_options.skill.app_id == to_bot_id_sent + assert dialog_options.skill.skill_endpoint == to_url_sent + assert activity_to_send.text == activity_sent.text + assert DialogTurnStatus.Waiting == client.dialog_turn_result.status + + await client.send_activity(MessageFactory.text("Second message")) + + assert activity_sent.text == "Second message" + assert DialogTurnStatus.Waiting == client.dialog_turn_result.status + + await client.send_activity(Activity(type=ActivityTypes.end_of_conversation)) + + assert DialogTurnStatus.Complete == client.dialog_turn_result.status + + def _create_skill_dialog_options( + self, conversation_state: ConversationState, skill_client: BotFrameworkClient + ): + return SkillDialogOptions( + bot_id=str(uuid.uuid4()), + skill_host_endpoint="http://test.contoso.com/skill/messages", + conversation_id_factory=SimpleConversationIdFactory(), + conversation_state=conversation_state, + skill_client=skill_client, + skill=BotFrameworkSkill( + app_id=str(uuid.uuid4()), + skill_endpoint="http://testskill.contoso.com/api/messages", + ), + ) + + def _create_mock_skill_client( + self, callback: Callable, return_status: int = 200 + ) -> BotFrameworkClient: + mock_client = Mock() + + async def mock_post_activity( + from_bot_id: str, + to_bot_id: str, + to_url: str, + service_url: str, + conversation_id: str, + activity: Activity, + ): + nonlocal callback, return_status + if callback: + await callback( + from_bot_id, + to_bot_id, + to_url, + service_url, + conversation_id, + activity, + ) + return InvokeResponse(status=return_status) + + mock_client.post_activity.side_effect = mock_post_activity + + return mock_client From c4e66d9dee870676ef25864590fa91954ab0a9cf Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Sat, 7 Mar 2020 06:39:29 -0600 Subject: [PATCH 21/38] Added remainder of SkillDialog unit tests --- .../tests/test_skill_dialog.py | 63 +++++++++++++++++++ .../botbuilder/testing/dialog_test_client.py | 7 ++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/libraries/botbuilder-dialogs/tests/test_skill_dialog.py b/libraries/botbuilder-dialogs/tests/test_skill_dialog.py index 9f9dfb5bb..cafa17c88 100644 --- a/libraries/botbuilder-dialogs/tests/test_skill_dialog.py +++ b/libraries/botbuilder-dialogs/tests/test_skill_dialog.py @@ -154,6 +154,69 @@ async def capture( assert DialogTurnStatus.Complete == client.dialog_turn_result.status + async def test_cancel_dialog_sends_eoc(self): + activity_sent = None + + async def capture( + from_bot_id: str, # pylint: disable=unused-argument + to_bot_id: str, # pylint: disable=unused-argument + to_url: str, # pylint: disable=unused-argument + service_url: str, # pylint: disable=unused-argument + conversation_id: str, # pylint: disable=unused-argument + activity: Activity, + ): + nonlocal activity_sent + activity_sent = activity + + mock_skill_client = self._create_mock_skill_client(capture) + + conversation_state = ConversationState(MemoryStorage()) + dialog_options = self._create_skill_dialog_options( + conversation_state, mock_skill_client + ) + + sut = SkillDialog(dialog_options, "dialog_id") + activity_to_send = MessageFactory.text(str(uuid.uuid4())) + + client = DialogTestClient( + "test", + sut, + BeginSkillDialogOptions(activity=activity_to_send), + conversation_state=conversation_state, + ) + + # Send something to the dialog to start it + await client.send_activity(MessageFactory.text("irrelevant")) + + # Cancel the dialog so it sends an EoC to the skill + await client.dialog_context.cancel_all_dialogs() + + assert activity_sent + assert activity_sent.type == ActivityTypes.end_of_conversation + + async def test_should_throw_on_post_failure(self): + # This mock client will fail + mock_skill_client = self._create_mock_skill_client(None, 500) + + conversation_state = ConversationState(MemoryStorage()) + dialog_options = self._create_skill_dialog_options( + conversation_state, mock_skill_client + ) + + sut = SkillDialog(dialog_options, "dialog_id") + activity_to_send = MessageFactory.text(str(uuid.uuid4())) + + client = DialogTestClient( + "test", + sut, + BeginSkillDialogOptions(activity=activity_to_send), + conversation_state=conversation_state, + ) + + # A send should raise an exception + with self.assertRaises(Exception): + await client.send_activity("irrelevant") + def _create_skill_dialog_options( self, conversation_state: ConversationState, skill_client: BotFrameworkClient ): diff --git a/libraries/botbuilder-testing/botbuilder/testing/dialog_test_client.py b/libraries/botbuilder-testing/botbuilder/testing/dialog_test_client.py index 2ca60fa07..3e284b5c9 100644 --- a/libraries/botbuilder-testing/botbuilder/testing/dialog_test_client.py +++ b/libraries/botbuilder-testing/botbuilder/testing/dialog_test_client.py @@ -54,6 +54,7 @@ def __init__( :type conversation_state: ConversationState """ self.dialog_turn_result: DialogTurnResult = None + self.dialog_context = None self.conversation_state: ConversationState = ( ConversationState(MemoryStorage()) if conversation_state is None @@ -108,10 +109,10 @@ async def default_callback(turn_context: TurnContext) -> None: dialog_set = DialogSet(dialog_state) dialog_set.add(target_dialog) - dialog_context = await dialog_set.create_context(turn_context) - self.dialog_turn_result = await dialog_context.continue_dialog() + self.dialog_context = await dialog_set.create_context(turn_context) + self.dialog_turn_result = await self.dialog_context.continue_dialog() if self.dialog_turn_result.status == DialogTurnStatus.Empty: - self.dialog_turn_result = await dialog_context.begin_dialog( + self.dialog_turn_result = await self.dialog_context.begin_dialog( target_dialog.id, initial_dialog_options ) From 5bcfd10748dc702a147545284612348985d9a0f7 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Sun, 8 Mar 2020 23:57:17 -0700 Subject: [PATCH 22/38] Updates on dialog-root-bot --- .../botbuilder/dialogs/__init__.py | 2 + .../dialog-root-bot/bots/root_bot.py | 12 +--- .../dialog-root-bot/dialogs/main_dialog.py | 58 +++++++++++++++---- .../dialog-root-bot/dialogs/tangent_dialog.py | 39 +++++++++++++ 4 files changed, 92 insertions(+), 19 deletions(-) create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/tangent_dialog.py diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py index 2d0f4050b..bf2c8ae32 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py @@ -18,6 +18,7 @@ from .dialog import Dialog from .waterfall_dialog import WaterfallDialog from .waterfall_step_context import WaterfallStepContext +from .dialog_extensions import DialogExtensions from .prompts import * from .choices import * from .skills import * @@ -46,5 +47,6 @@ "Prompt", "PromptOptions", "TextPrompt", + "DialogExtensions", "__version__", ] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py index 23ab7f49a..403c4c1b0 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py @@ -9,15 +9,9 @@ MessageFactory, TurnContext, ) -from botbuilder.core.skills import SkillConversationIdFactory -from botbuilder.integration.aiohttp import BotFrameworkHttpClient -from botbuilder.dialogs import Dialog - +from botbuilder.dialogs import Dialog, DialogExtensions from botbuilder.schema import ActivityTypes, Attachment, ChannelAccount -from config import DefaultConfig, SkillConfiguration -from helpers.dialog_helper import DialogHelper - class RootBot(ActivityHandler): def __init__( @@ -30,7 +24,7 @@ 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 - await DialogHelper.run_dialog( + await DialogExtensions.run_dialog( self._main_dialog, turn_context, self._conversation_state.create_property("DialogState"), @@ -46,7 +40,7 @@ async def on_members_added_activity( welcome_card = self._create_adaptive_card_attachment() activity = MessageFactory.attachment(welcome_card) await turn_context.send_activity(activity) - await DialogHelper.run_dialog( + await DialogExtensions.run_dialog( self._main_dialog, turn_context, self._conversation_state.create_property("DialogState"), diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py index e86f63258..3d0009500 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py @@ -6,19 +6,21 @@ from jsonpickle import encode from botbuilder.dialogs import ( ComponentDialog, + DialogContext, WaterfallDialog, WaterfallStepContext, DialogTurnResult, ) from botbuilder.dialogs.choices import Choice, ListStyle from botbuilder.dialogs.prompts import PromptOptions, ChoicePrompt -from botbuilder.dialogs.skills import SkillDialogOptions, SkillDialog, SkillDialogArgs +from botbuilder.dialogs.skills import SkillDialogOptions, SkillDialog, BeginSkillDialogOptions from botbuilder.core import ConversationState, MessageFactory from botbuilder.core.skills import BotFrameworkSkill, ConversationIdFactoryBase -from botbuilder.schema import Activity, ActivityTypes, InputHints +from botbuilder.schema import Activity, ActivityTypes, InputHints, DeliveryModes from botbuilder.integration.aiohttp.skills import SkillHttpClient from .booking_details import BookingDetails +from .tangent_dialog import TangentDialog from ..config import DefaultConfig, SkillConfiguration @@ -75,14 +77,38 @@ def __init__( self.add_dialog( WaterfallDialog( WaterfallDialog.__name__, - [self.intro_step, self.act_step, self.final_step], + [ + self.select_skill_step, + self.select_skill_action_step, + self.call_skill_action_step, + self.final_step + ], ) ) - self._active_skill_property = conversation_state.create_property() + self._active_skill_property = conversation_state.create_property(MainDialog.ACTIVE_SKILL_PROPERTY_NAME) 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 + active_skill = await self._active_skill_property.get(inner_dc.context) + activity = inner_dc.context.activity + if active_skill and activity.type == ActivityTypes.message and "abort" in activity.text: + # Cancel all dialog when the user says abort. + await inner_dc.cancel_all_dialogs() + return await inner_dc.replace_dialog( + self.initial_dialog_id + ) + + elif active_skill and activity.type == ActivityTypes.message and "tangent" in activity.text: + # Begin Tangent + return await inner_dc.replace_dialog( + TangentDialog.__name__ + ) + + return super().on_continue_dialog(inner_dc) + async def select_skill_step( self, step_context: WaterfallStepContext ) -> DialogTurnResult: @@ -106,7 +132,7 @@ async def select_skill_action_step( selected_skill = next( ( skill - for skill in self._skills_config.SKILLS + for skill_id, skill in self._skills_config.SKILLS if skill.id == selected_skill_id ), None, @@ -147,11 +173,11 @@ async def call_skill_action_step( text="Start echo skill", ) elif selected_skill.id == "DialogSkillBot": - skill_activity = self._get_dialog_skill_bot_activity(step_context.result) + skill_activity = self._create_dialog_skill_bot_activity(step_context.result.value) else: raise Exception(f"Unknown target skill id: {selected_skill.id}.") - skill_dialog_args = SkillDialogArgs(selected_skill, skill_activity) + skill_dialog_args = BeginSkillDialogOptions(skill_activity) # We are manually creating the activity to send to the skill, ensure we add the ChannelData and Properties # from the original activity so the skill gets them. @@ -163,9 +189,15 @@ async def call_skill_action_step( step_context.context.activity.additional_properties ) - return await step_context.begin_dialog(SkillDialog.__name__, skill_dialog_args) + skill_dialog_args.activity.delivery_mode = DeliveryModes.expect_replies + + await self._active_skill_property.set(step_context.context, selected_skill) + + return await step_context.begin_dialog(selected_skill.id, skill_dialog_args) async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + active_skill = await self._active_skill_property.get(step_context.context) + if step_context.result: message = "Skill invocation complete." message += f" Result: {encode(step_context.result)}" @@ -173,9 +205,15 @@ async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResu MessageFactory.text(message, input_hint=InputHints.ignoring_input) ) + # Clear the skill selected by the user. + step_context.values[self._selected_skill_key] = None + + # 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 return await step_context.replace_dialog( - self.initial_dialog_id, "What else can I do for you?" + self.initial_dialog_id ) # Helper method to create Choice elements for the actions supported by the skill @@ -197,7 +235,7 @@ def _get_skill_actions(self, skill: BotFrameworkSkill) -> List[Choice]: return choices # Helper method to create the activity to be sent to the DialogSkillBot - def _get_dialog_skill_bot_activity(self, selected_option: str) -> Activity: + def _create_dialog_skill_bot_activity(self, selected_option: str) -> Activity: # Note: in a real bot, the dialogArgs will be created dynamically based on the conversation # and what each action requires, this code hardcodes the values to make things simpler. diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/tangent_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/tangent_dialog.py new file mode 100644 index 000000000..a0d300788 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/tangent_dialog.py @@ -0,0 +1,39 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core import MessageFactory +from botbuilder.dialogs import ComponentDialog, DialogTurnResult, WaterfallDialog, WaterfallStepContext +from botbuilder.dialogs.prompts import TextPrompt, PromptOptions +from botbuilder.schema import InputHints + + +class TangentDialog(ComponentDialog): + def __init__(self, dialog_id: str = "TangentDialog"): + super().__init__(dialog_id) + + self.add_dialog(TextPrompt(TextPrompt.__name__)) + self.add_dialog( + WaterfallDialog( + WaterfallDialog.__name__, + [ + self.step_1, + self.step_2 + ], + ) + ) + + self.initial_dialog_id = WaterfallDialog.__name__ + + async def step_1( + self, step_context: WaterfallStepContext + ) -> DialogTurnResult: + prompt_message = MessageFactory.text("Tangent step 1 of 2", InputHints.expecting_input) + + return await step_context.prompt(TextPrompt.__name__, PromptOptions(prompt=prompt_message)) + + async def step_2( + self, step_context: WaterfallStepContext + ) -> DialogTurnResult: + prompt_message = MessageFactory.text("Tangent step 2 of 2", InputHints.expecting_input) + + return await step_context.prompt(TextPrompt.__name__, PromptOptions(prompt=prompt_message)) From 3e031c2933ec8b769beefe4149c100dc539ab764 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Mon, 9 Mar 2020 07:58:36 -0500 Subject: [PATCH 23/38] Updated buffered_replies to expect_replies --- .../botbuilder/dialogs/skills/skill_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py index 0d30d39d9..ecdbf7247 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -217,7 +217,7 @@ async def _send_to_skill( ) eoc_activity: Activity = None - if activity.delivery_mode == DeliveryModes.buffered_replies and response.body: + if activity.delivery_mode == DeliveryModes.expect_replies and response.body: # Process replies in the response.Body. response.body: List[Activity] for from_skill_activity in response.body: From 09cd3cc5b3b091a37655bed33cbcdc4f129eb67f Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Mon, 9 Mar 2020 09:36:30 -0500 Subject: [PATCH 24/38] Using HTTPStatus defines. --- .../botbuilder/dialogs/prompts/oauth_prompt.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py index 0f9a82453..be5373c29 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py @@ -3,6 +3,7 @@ import re from datetime import datetime, timedelta +from http import HTTPStatus from typing import Union, Awaitable, Callable from botframework.connector import Channels @@ -377,15 +378,23 @@ async def _recognize_token(self, context: TurnContext) -> PromptRecognizerResult token = await self.get_user_token(context, code) if token is not None: await context.send_activity( - Activity(type="invokeResponse", value=InvokeResponse(200)) + Activity( + type="invokeResponse", value=InvokeResponse(HTTPStatus.OK) + ) ) else: await context.send_activity( - Activity(type="invokeResponse", value=InvokeResponse(404)) + Activity( + type="invokeResponse", + value=InvokeResponse(HTTPStatus.NOT_FOUND), + ) ) except Exception: - context.send_activity( - Activity(type="invokeResponse", value=InvokeResponse(500)) + await context.send_activity( + Activity( + type="invokeResponse", + value=InvokeResponse(HTTPStatus.INTERNAL_SERVER_ERROR), + ) ) elif context.activity.type == ActivityTypes.message and context.activity.text: match = re.match(r"(? Date: Mon, 9 Mar 2020 10:42:36 -0500 Subject: [PATCH 25/38] Skill OAuth only change card action for emulator --- .../botbuilder/dialogs/prompts/oauth_prompt.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py index be5373c29..a6f9bf1a4 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py @@ -14,7 +14,7 @@ InvokeResponse, TurnContext, UserTokenProvider, -) + BotAdapter) from botbuilder.dialogs import Dialog, DialogContext, DialogTurnResult from botbuilder.schema import ( Activity, @@ -293,7 +293,7 @@ async def _send_oauth_card( ): link = None card_action_type = ActionTypes.signin - bot_identity: ClaimsIdentity = context.turn_state.get("BotIdentity") + bot_identity: ClaimsIdentity = context.turn_state.get(BotAdapter.BOT_IDENTITY_KEY) # check if it's from streaming connection if not context.activity.service_url.startswith("http"): @@ -316,7 +316,9 @@ async def _send_oauth_card( None, self._settings.oath_app_credentials, ) - card_action_type = ActionTypes.open_url + + if context.activity.channel_id == "emulator": + card_action_type = ActionTypes.open_url prompt.attachments.append( CardFactory.oauth_card( From 97e691b10dbe2742b3df35ffefad6b191de067a7 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Mon, 9 Mar 2020 10:46:41 -0500 Subject: [PATCH 26/38] black --- .../botbuilder/dialogs/prompts/oauth_prompt.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py index a6f9bf1a4..87db5767c 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py @@ -14,7 +14,8 @@ InvokeResponse, TurnContext, UserTokenProvider, - BotAdapter) + BotAdapter, +) from botbuilder.dialogs import Dialog, DialogContext, DialogTurnResult from botbuilder.schema import ( Activity, @@ -293,7 +294,9 @@ async def _send_oauth_card( ): link = None card_action_type = ActionTypes.signin - bot_identity: ClaimsIdentity = context.turn_state.get(BotAdapter.BOT_IDENTITY_KEY) + bot_identity: ClaimsIdentity = context.turn_state.get( + BotAdapter.BOT_IDENTITY_KEY + ) # check if it's from streaming connection if not context.activity.service_url.startswith("http"): From 8e9c573e8d56aa72d6aa6a8712377cb6d3fe493f Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Mon, 9 Mar 2020 09:31:07 -0700 Subject: [PATCH 27/38] skill root bot updated --- .../botbuilder/dialogs/skills/skill_dialog.py | 4 +- .../adapter_with_error_handler.py | 44 ++++++++++++++--- .../dialog-to-dialog/dialog-root-bot/app.py | 19 ++++++-- .../dialog-root-bot/dialogs/__init__.py | 11 +++++ .../skill_conversation_id_factory.py | 47 +++++++++++++++++++ 5 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/__init__.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py index ecdbf7247..58c3857e0 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -4,7 +4,7 @@ from copy import deepcopy from typing import List -from botbuilder.schema import Activity, ActivityTypes, DeliveryModes +from botbuilder.schema import Activity, ActivityTypes, ExpectedReplies, DeliveryModes from botbuilder.core import ( BotAdapter, TurnContext, @@ -220,6 +220,8 @@ async def _send_to_skill( if activity.delivery_mode == DeliveryModes.expect_replies and response.body: # Process replies in the response.Body. response.body: List[Activity] + response.body = ExpectedReplies().deserialize(response.body).activities + for from_skill_activity in response.body: if from_skill_activity.type == ActivityTypes.end_of_conversation: # Capture the EndOfConversation activity if it was sent from skill diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py index 4e24fcb79..9ae38e44a 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py @@ -8,16 +8,17 @@ BotFrameworkAdapter, BotFrameworkAdapterSettings, ConversationState, + MessageFactory, TurnContext, ) -from botbuilder.schema import ActivityTypes, Activity +from botbuilder.schema import ActivityTypes, Activity, InputHints class AdapterWithErrorHandler(BotFrameworkAdapter): def __init__( self, settings: BotFrameworkAdapterSettings, - conversation_state: ConversationState, + conversation_state: ConversationState = None, ): super().__init__(settings) self._conversation_state = conversation_state @@ -31,10 +32,14 @@ async def on_error(context: TurnContext, error: Exception): traceback.print_exc() # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) + error_message_text = "The skill encountered an error or bug." + error_message = MessageFactory.text(error_message_text, error_message_text, InputHints.ignoring_input) + await context.send_activity(error_message) + + error_message_text = "To continue to run this bot, please fix the bot source code." + error_message = MessageFactory.text(error_message_text, error_message_text, InputHints.ignoring_input) + await context.send_activity(error_message) + # Send a trace activity if we're talking to the Bot Framework Emulator if context.activity.channel_id == "emulator": # Create a trace activity that contains the error object @@ -51,6 +56,31 @@ async def on_error(context: TurnContext, error: Exception): # Clear out state nonlocal self - await self._conversation_state.delete(context) + if self._conversation_state: + try: + await self._conversation_state.delete(context) + except Exception as exception: + print( + f"\n Exception caught on attempting to Delete ConversationState : {exception}", + file=sys.stderr + ) + traceback.print_exc() + + # Send and EndOfConversation activity to the skill caller with the error to end the conversation + # and let the caller decide what to do. + end_of_conversation = Activity(type=ActivityTypes.end_of_conversation) + end_of_conversation.code = "SkillError" + end_of_conversation.text = str(error) + await context.send_activity(end_of_conversation) + + # Send a trace activity, which will be displayed in the Bot Framework Emulator + # Note: we return the entire exception in the value property to help the developer, + # this should not be done in prod. + await context.send_trace_activity( + "OnTurnError Trace", + str(error), + "https://www.botframework.com/schemas/error", + "TurnError" + ) self.on_turn_error = on_error diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py index 3a2e5193f..fa9f9a0d9 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py @@ -16,39 +16,48 @@ aiohttp_error_middleware, BotFrameworkHttpClient ) -from botbuilder.core.skills import SkillConversationIdFactory, SkillHandler +from botbuilder.core.skills import SkillHandler from botbuilder.schema import Activity, ActivityTypes +from botbuilder.integration.aiohttp.skills import SkillHttpClient from botframework.connector.auth import ( AuthenticationConfiguration, SimpleCredentialProvider, ) from bots import RootBot +from dialogs import MainDialog from config import DefaultConfig, SkillConfiguration from adapter_with_error_handler import AdapterWithErrorHandler +from authentication.allow_callers_claims_validation import AllowedCallersClaimsValidator +from skill_conversation_id_factory import SkillConversationIdFactory CONFIG = DefaultConfig() SKILL_CONFIG = SkillConfiguration() -CREDENTIAL_PROVIDER = SimpleCredentialProvider(CONFIG.APP_ID, CONFIG.APP_PASSWORD) -CLIENT = BotFrameworkHttpClient(CREDENTIAL_PROVIDER) - # Create MemoryStorage, UserState and ConversationState MEMORY = MemoryStorage() USER_STATE = UserState(MEMORY) CONVERSATION_STATE = ConversationState(MEMORY) ID_FACTORY = SkillConversationIdFactory(MEMORY) +CREDENTIAL_PROVIDER = SimpleCredentialProvider(CONFIG.APP_ID, CONFIG.APP_PASSWORD) +CLIENT = SkillHttpClient(CREDENTIAL_PROVIDER, ID_FACTORY) + + # Create adapter. # See https://aka.ms/about-bot-adapter to learn more about how bots work. SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD) ADAPTER = AdapterWithErrorHandler(SETTINGS, CONVERSATION_STATE) +DIALOG = MainDialog(CONVERSATION_STATE, ID_FACTORY, CLIENT, SKILL_CONFIG, CONFIG) + # Create the Bot BOT = RootBot(CONVERSATION_STATE, SKILL_CONFIG, ID_FACTORY, CLIENT, CONFIG) +AUTH_CONFIG = AuthenticationConfiguration(claims_validator=AllowedCallersClaimsValidator(CONFIG).claims_validator) + SKILL_HANDLER = SkillHandler( - ADAPTER, BOT, ID_FACTORY, CREDENTIAL_PROVIDER, AuthenticationConfiguration() + ADAPTER, BOT, ID_FACTORY, CREDENTIAL_PROVIDER, AUTH_CONFIG ) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/__init__.py new file mode 100644 index 000000000..d490c7656 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/__init__.py @@ -0,0 +1,11 @@ +from .booking_details import BookingDetails +from .location import Location +from .main_dialog import MainDialog +from .tangent_dialog import TangentDialog + +__all__ = [ + "BookingDetails", + "Location", + "MainDialog", + "TangentDialog" +] \ No newline at end of file diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py new file mode 100644 index 000000000..8faaae025 --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py @@ -0,0 +1,47 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core import Storage +from botbuilder.core.skills import ConversationIdFactoryBase +from botbuilder.schema import ConversationReference + + +class SkillConversationIdFactory(ConversationIdFactoryBase): + def __init__(self, storage: Storage): + if not storage: + raise TypeError("storage can't be None") + + self._storage = storage + + async def create_skill_conversation_id( + self, conversation_reference: ConversationReference + ) -> str: + if not conversation_reference: + raise TypeError("conversation_reference can't be None") + + if not conversation_reference.conversation.id: + raise TypeError("conversation id in conversation reference can't be None") + + if not conversation_reference.channel_id: + raise TypeError("channel id in conversation reference can't be None") + + storage_key = f"{conversation_reference.channel_id}:{conversation_reference.conversation.id}" + + skill_conversation_info = {storage_key: conversation_reference} + + await self._storage.write(skill_conversation_info) + + return storage_key + + async def get_conversation_reference( + self, skill_conversation_id: str + ) -> ConversationReference: + if not skill_conversation_id: + raise TypeError("skill_conversation_id can't be None") + + skill_conversation_info = await self._storage.read([skill_conversation_id]) + + return skill_conversation_info.get(skill_conversation_id) + + async def delete_conversation_reference(self, skill_conversation_id: str): + await self._storage.delete([skill_conversation_id]) From c224ea7f2844a0b09972bfd5f6d7bbff1b9441d7 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Mon, 9 Mar 2020 09:32:13 -0700 Subject: [PATCH 28/38] skill root bot updated --- .../skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py index fa9f9a0d9..34496e241 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py @@ -7,9 +7,7 @@ BotFrameworkAdapterSettings, ConversationState, MemoryStorage, - TurnContext, UserState, - BotFrameworkAdapter, ) from botbuilder.core.integration import ( aiohttp_channel_service_routes, @@ -17,7 +15,7 @@ BotFrameworkHttpClient ) from botbuilder.core.skills import SkillHandler -from botbuilder.schema import Activity, ActivityTypes +from botbuilder.schema import Activity from botbuilder.integration.aiohttp.skills import SkillHttpClient from botframework.connector.auth import ( AuthenticationConfiguration, From d9a0b0590f25b32e02acbf1717fd48125289ce3f Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Mon, 9 Mar 2020 09:34:06 -0700 Subject: [PATCH 29/38] Removed old import in dialog root bot --- .../skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py index 34496e241..26dba26ef 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py @@ -12,7 +12,6 @@ from botbuilder.core.integration import ( aiohttp_channel_service_routes, aiohttp_error_middleware, - BotFrameworkHttpClient ) from botbuilder.core.skills import SkillHandler from botbuilder.schema import Activity From 91486b4fbcb64ea2f737e4aa5111ecdd755c14b2 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Mon, 9 Mar 2020 17:13:12 -0500 Subject: [PATCH 30/38] Dialog-to-dialog work --- .../dialog-skill-bot/README.md | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/README.md diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/README.md b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/README.md deleted file mode 100644 index 299259d5b..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# EchoBot - -Bot Framework v4 echo bot sample. - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-samples.git -``` -- Bring up a terminal, navigate to `botbuilder-samples\samples\python\02.echo-bot` folder -- Activate your desired virtual environment -- In the terminal, type `pip install -r requirements.txt` -- Run your bot with `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- Paste this URL in the emulator window - http://localhost:3978/api/messages - -## Deploy the bot to Azure - -To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions. - -# Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) -- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) -- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) -- [Azure Portal](https://portal.azure.com) From 36afefcbaf9910cec3181d494abd5751afe46cd9 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Mon, 9 Mar 2020 17:13:41 -0500 Subject: [PATCH 31/38] Ummm... the actual dialog-to-dialog work --- .../dialog-echo-skill-bot/config.py | 6 +- .../dialog-to-dialog/dialog-root-bot/app.py | 12 +-- .../authentication/__init__.py | 4 +- .../allow_callers_claims_validation.py | 39 --------- .../allowed_skills_claims_validator.py | 30 +++++++ .../dialog-root-bot/bots/root_bot.py | 5 +- .../dialog-root-bot/config.py | 21 +++-- .../dialog-root-bot/dialogs/main_dialog.py | 64 +++++++++------ .../skill_conversation_id_factory.py | 42 +++++++--- .../dialog-to-dialog/dialog-skill-bot/app.py | 7 +- .../allow_callers_claims_validation.py | 4 +- .../dialog-skill-bot/bots/__init__.py | 5 +- .../bots/activity_router_dialog.py | 82 ++++++++++--------- .../dialog-skill-bot/bots/skill_bot.py | 67 ++++----------- .../dialog-skill-bot/config.py | 18 ++-- .../dialogs/dialog_skill_bot_recognizer.py | 2 +- .../dialogs/oauth_test_dialog.py | 2 +- .../dialog-skill-bot/requirements.txt | 3 +- 18 files changed, 210 insertions(+), 203 deletions(-) delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allow_callers_claims_validation.py create mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allowed_skills_claims_validator.py diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py index e007d0fa9..4162719f0 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py @@ -10,6 +10,6 @@ class DefaultConfig: """ Bot Configuration """ - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") + PORT = 39793 + APP_ID = os.environ.get("MicrosoftAppId", "fb7a9f3c-2b30-4ac8-86a0-c44bdeaa380e") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "b0tframew0rks3cr3t!") diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py index 26dba26ef..037e5da25 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py @@ -21,11 +21,11 @@ SimpleCredentialProvider, ) +from authentication import AllowedSkillsClaimsValidator from bots import RootBot from dialogs import MainDialog from config import DefaultConfig, SkillConfiguration from adapter_with_error_handler import AdapterWithErrorHandler -from authentication.allow_callers_claims_validation import AllowedCallersClaimsValidator from skill_conversation_id_factory import SkillConversationIdFactory CONFIG = DefaultConfig() @@ -49,14 +49,14 @@ DIALOG = MainDialog(CONVERSATION_STATE, ID_FACTORY, CLIENT, SKILL_CONFIG, CONFIG) # Create the Bot -BOT = RootBot(CONVERSATION_STATE, SKILL_CONFIG, ID_FACTORY, CLIENT, CONFIG) +BOT = RootBot(CONVERSATION_STATE, DIALOG) # , SKILL_CONFIG, ID_FACTORY, CLIENT, CONFIG) -AUTH_CONFIG = AuthenticationConfiguration(claims_validator=AllowedCallersClaimsValidator(CONFIG).claims_validator) - -SKILL_HANDLER = SkillHandler( - ADAPTER, BOT, ID_FACTORY, CREDENTIAL_PROVIDER, AUTH_CONFIG +AUTH_CONFIG = AuthenticationConfiguration( + claims_validator=AllowedSkillsClaimsValidator(CONFIG).validate_claims ) +SKILL_HANDLER = SkillHandler(ADAPTER, BOT, ID_FACTORY, CREDENTIAL_PROVIDER, AUTH_CONFIG) + # Listen for incoming requests on /api/messages async def messages(req: Request) -> Response: diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/__init__.py index ebbe2ac15..f60882d12 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/__init__.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/__init__.py @@ -1,3 +1,3 @@ -from .allow_callers_claims_validation import AllowedCallersClaimsValidator +from .allowed_skills_claims_validator import AllowedSkillsClaimsValidator -__all__ = ["AllowedCallersClaimsValidator"] +__all__ = ["AllowedSkillsClaimsValidator"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allow_callers_claims_validation.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allow_callers_claims_validation.py deleted file mode 100644 index 2d5b30060..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allow_callers_claims_validation.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import Awaitable, Callable, Dict, List - -from botframework.connector.auth import JwtTokenValidation, SkillValidation - -from ..config import DefaultConfig - - -class AllowedCallersClaimsValidator: - - config_key = "ALLOWED_CALLERS" - - def __init__(self, config: DefaultConfig): - if not config: - raise TypeError( - "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 - self._allowed_callers = getattr(config) - - @property - def claims_validator(self) -> Callable[[List[Dict]], Awaitable]: - async def allow_callers_claims_validator(claims: Dict[str, object]): - # if _allowedCallers is None we allow all calls - if 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. - app_id = JwtTokenValidation.get_app_id_from_claims(claims) - if app_id not in self._allowed_callers: - raise PermissionError( - f'Received a request from a bot with an app ID of "{app_id}".' - f" To enable requests from this caller, add the app ID to your configuration file." - ) - - return - - return allow_callers_claims_validator diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allowed_skills_claims_validator.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allowed_skills_claims_validator.py new file mode 100644 index 000000000..55b0af43d --- /dev/null +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allowed_skills_claims_validator.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botframework.connector.auth import JwtTokenValidation, SkillValidation + +from config import DefaultConfig + + +class AllowedSkillsClaimsValidator: + """ + Sample claims validator that loads an allowed list from config if present and checks + that requests are coming from allowed skills. + """ + + def __init__(self, config: DefaultConfig): + self.allowed_callers = config.SKILLS + + # Check AppIds for the configured callers (we will only allow responses from skills we have configured). + # SkillConfiguration.SKILLS is the list of Skill app Ids that are allowed to access the parent. + # To add a new skill simply go to the config.py file and add + # the skill's id, Microsoft AppId and skill_endpoint to the array under SKILLS. + async def validate_claims(self, claims: dict): + if SkillValidation.is_skill_claim(claims) and self.allowed_callers: + # Check that the appId claim in the request is in the list of skills configured for this bot. + app_id = JwtTokenValidation.get_app_id_from_claims(claims) + if app_id not in self.allowed_callers: + raise ValueError( + f'Received a request from an application with an appID of "{ app_id }". To enable requests ' + f"from this skill, add the id to your configuration file." + ) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py index 403c4c1b0..402fe128d 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py @@ -21,7 +21,7 @@ def __init__( self._main_dialog = main_dialog async def on_turn(self, turn_context: TurnContext): - if turn_context.activity.type == ActivityTypes.conversation_update: + if turn_context.activity.type != ActivityTypes.conversation_update: # Handle end of conversation back from the skill # forget skill invocation await DialogExtensions.run_dialog( @@ -32,6 +32,9 @@ async def on_turn(self, turn_context: TurnContext): else: await super().on_turn(turn_context) + # Save any state changes that might have occurred during the turn. + await self._conversation_state.save_changes(turn_context) + async def on_members_added_activity( self, members_added: List[ChannelAccount], turn_context: TurnContext ): diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py index f2a9e1f6e..1bf1201ad 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py @@ -6,21 +6,24 @@ from typing import Dict from botbuilder.core.skills import BotFrameworkSkill -""" Bot Configuration """ - class DefaultConfig: """ Bot Configuration """ - PORT = 3428 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") - SKILL_HOST_ENDPOINT = "http://localhost:3428/api/skills" + PORT = 3978 + APP_ID = os.environ.get("MicrosoftAppId", "b56a1b59-7081-4546-b3fa-177401fd0657") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "b0tframew0rks3cr3t!") + SKILL_HOST_ENDPOINT = "http://localhost:3978/api/skills" SKILLS = [ { - "id": "SkillBot", - "app_id": "", - "skill_endpoint": "http://localhost:3978/api/messages", + "id": "EchoSkillBot", + "app_id": "fb7a9f3c-2b30-4ac8-86a0-c44bdeaa380e", + "skill_endpoint": "http://localhost:39793/api/messages", + }, + { + "id": "DialogSkillBot", + "app_id": "67ec4e96-f4f6-424b-911f-de362a2a81d4", + "skill_endpoint": "http://localhost:39783/api/messages", }, ] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py index 3d0009500..2dcffe3ee 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py @@ -13,15 +13,19 @@ ) from botbuilder.dialogs.choices import Choice, ListStyle from botbuilder.dialogs.prompts import PromptOptions, ChoicePrompt -from botbuilder.dialogs.skills import SkillDialogOptions, SkillDialog, BeginSkillDialogOptions +from botbuilder.dialogs.skills import ( + SkillDialogOptions, + SkillDialog, + BeginSkillDialogOptions, +) from botbuilder.core import ConversationState, MessageFactory from botbuilder.core.skills import BotFrameworkSkill, ConversationIdFactoryBase from botbuilder.schema import Activity, ActivityTypes, InputHints, DeliveryModes from botbuilder.integration.aiohttp.skills import SkillHttpClient +from config import SkillConfiguration, DefaultConfig from .booking_details import BookingDetails from .tangent_dialog import TangentDialog -from ..config import DefaultConfig, SkillConfiguration class MainDialog(ComponentDialog): @@ -59,6 +63,9 @@ def __init__( # ChoicePrompt to render available skills and skill actions self.add_dialog(ChoicePrompt(ChoicePrompt.__name__)) + # Register the tangent + self.add_dialog(TangentDialog()) + # Create SkillDialog instances for the configured skills for _, skill_info in skills_config.SKILLS.items(): # SkillDialog used to wrap interaction with the selected skill @@ -68,7 +75,7 @@ def __init__( skill_client=skill_client, skill=skill_info, skill_host_endpoint=skills_config.SKILL_HOST_ENDPOINT, - conversation_state=conversation_state + conversation_state=conversation_state, ) self.add_dialog(SkillDialog(skill_dialog_options, skill_info.id)) @@ -81,12 +88,14 @@ def __init__( self.select_skill_step, self.select_skill_action_step, self.call_skill_action_step, - self.final_step + self.final_step, ], ) ) - self._active_skill_property = conversation_state.create_property(MainDialog.ACTIVE_SKILL_PROPERTY_NAME) + self._active_skill_property = conversation_state.create_property( + MainDialog.ACTIVE_SKILL_PROPERTY_NAME + ) self.initial_dialog_id = WaterfallDialog.__name__ @@ -94,20 +103,25 @@ 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 active_skill = await self._active_skill_property.get(inner_dc.context) activity = inner_dc.context.activity - if active_skill and activity.type == ActivityTypes.message and "abort" in activity.text: + + if ( + active_skill + and activity.type == ActivityTypes.message + and "abort" in activity.text + ): # Cancel all dialog when the user says abort. await inner_dc.cancel_all_dialogs() - return await inner_dc.replace_dialog( - self.initial_dialog_id - ) + return await inner_dc.replace_dialog(self.initial_dialog_id) - elif active_skill and activity.type == ActivityTypes.message and "tangent" in activity.text: + if ( + active_skill + and activity.type == ActivityTypes.message + and "tangent" in activity.text + ): # Begin Tangent - return await inner_dc.replace_dialog( - TangentDialog.__name__ - ) + return await inner_dc.replace_dialog(TangentDialog.__name__) - return super().on_continue_dialog(inner_dc) + return await super().on_continue_dialog(inner_dc) async def select_skill_step( self, step_context: WaterfallStepContext @@ -118,7 +132,10 @@ async def select_skill_step( retry_prompt=MessageFactory.text( "That was not a valid choice, please select a valid skill." ), - choices=[Choice(value=skill.id) for skill in self._skills_config.SKILLS], + choices=[ + Choice(value=skill.id) + for _, skill in self._skills_config.SKILLS.items() + ], ) # Prompt the user to select a skill. @@ -129,14 +146,7 @@ async def select_skill_action_step( ) -> DialogTurnResult: # Get the skill info based on the selected skill. selected_skill_id = step_context.result - selected_skill = next( - ( - skill - for skill_id, skill in self._skills_config.SKILLS - if skill.id == selected_skill_id - ), - None, - ) + selected_skill = self._skills_config.SKILLS.get(selected_skill_id.value) # Remember the skill selected by the user. step_context.values[self._selected_skill_key] = selected_skill @@ -173,7 +183,9 @@ async def call_skill_action_step( text="Start echo skill", ) elif selected_skill.id == "DialogSkillBot": - skill_activity = self._create_dialog_skill_bot_activity(step_context.result.value) + skill_activity = self._create_dialog_skill_bot_activity( + step_context.result.value + ) else: raise Exception(f"Unknown target skill id: {selected_skill.id}.") @@ -212,9 +224,7 @@ async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResu await self._active_skill_property.delete(step_context.context) # Restart the main dialog with a different message the second time around - return await step_context.replace_dialog( - self.initial_dialog_id - ) + return await step_context.replace_dialog(self.initial_dialog_id) # Helper method to create Choice elements for the actions supported by the skill def _get_skill_actions(self, skill: BotFrameworkSkill) -> List[Choice]: diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py index 8faaae025..5d33d602e 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py @@ -1,8 +1,13 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from typing import Union -from botbuilder.core import Storage -from botbuilder.core.skills import ConversationIdFactoryBase +from botbuilder.core import Storage, TurnContext +from botbuilder.core.skills import ( + ConversationIdFactoryBase, + SkillConversationIdFactoryOptions, + SkillConversationReference, +) from botbuilder.schema import ConversationReference @@ -14,20 +19,33 @@ def __init__(self, storage: Storage): self._storage = storage async def create_skill_conversation_id( - self, conversation_reference: ConversationReference + self, + options_or_conversation_reference: Union[ + SkillConversationIdFactoryOptions, ConversationReference + ], ) -> str: - if not conversation_reference: - raise TypeError("conversation_reference can't be None") + if not options_or_conversation_reference: + raise TypeError("Need options or conversation reference") - if not conversation_reference.conversation.id: - raise TypeError("conversation id in conversation reference can't be None") - - if not conversation_reference.channel_id: - raise TypeError("channel id in conversation reference can't be None") + if not isinstance( + options_or_conversation_reference, SkillConversationIdFactoryOptions + ): + raise TypeError( + "This SkillConversationIdFactory can only handle SkillConversationIdFactoryOptions" + ) + options = options_or_conversation_reference + conversation_reference = TurnContext.get_conversation_reference( + options.activity + ) storage_key = f"{conversation_reference.channel_id}:{conversation_reference.conversation.id}" - skill_conversation_info = {storage_key: conversation_reference} + skill_conversation_reference = SkillConversationReference( + conversation_reference=conversation_reference, + oauth_scope=options.from_bot_oauth_scope, + ) + + skill_conversation_info = {storage_key: skill_conversation_reference} await self._storage.write(skill_conversation_info) @@ -35,7 +53,7 @@ async def create_skill_conversation_id( async def get_conversation_reference( self, skill_conversation_id: str - ) -> ConversationReference: + ) -> Union[SkillConversationReference, ConversationReference]: if not skill_conversation_id: raise TypeError("skill_conversation_id can't be None") diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/app.py index 17a9410a1..a877a4e11 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/app.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/app.py @@ -14,8 +14,9 @@ from botframework.connector.auth import AuthenticationConfiguration from authentication import AllowedCallersClaimsValidator -from bots import EchoBot +from bots import SkillBot, ActivityRouterDialog from config import DefaultConfig +from dialogs import DialogSkillBotRecognizer from skill_adapter_with_error_handler import SkillAdapterWithErrorHandler CONFIG = DefaultConfig() @@ -36,7 +37,9 @@ ADAPTER = SkillAdapterWithErrorHandler(SETTINGS, CONVERSATION_STATE) # Create the Bot -BOT = EchoBot() +RECOGNIZER = DialogSkillBotRecognizer(CONFIG) +ROUTER = ActivityRouterDialog(RECOGNIZER, CONFIG) +BOT = SkillBot(CONVERSATION_STATE, ROUTER) # Listen for incoming requests on /api/messages diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/allow_callers_claims_validation.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/allow_callers_claims_validation.py index 2d5b30060..3403a32bc 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/allow_callers_claims_validation.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/allow_callers_claims_validation.py @@ -2,7 +2,7 @@ from botframework.connector.auth import JwtTokenValidation, SkillValidation -from ..config import DefaultConfig +from config import DefaultConfig class AllowedCallersClaimsValidator: @@ -19,7 +19,7 @@ def __init__(self, config: DefaultConfig): # 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 - self._allowed_callers = getattr(config) + self._allowed_callers = config.ALLOWED_CALLERS @property def claims_validator(self) -> Callable[[List[Dict]], Awaitable]: diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/__init__.py index f95fbbbad..4d7e002ab 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/__init__.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/__init__.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from .echo_bot import EchoBot +from .skill_bot import SkillBot +from .activity_router_dialog import ActivityRouterDialog -__all__ = ["EchoBot"] +__all__ = ["SkillBot", "ActivityRouterDialog"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py index b2816236a..7bcaac203 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py @@ -1,29 +1,32 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from http import HTTPStatus -from typing import List - -from jsonpickle import encode from botbuilder.dialogs import ( ComponentDialog, WaterfallDialog, WaterfallStepContext, DialogTurnResult, - DialogTurnStatus + DialogTurnStatus, ) -from botbuilder.dialogs.choices import Choice, ListStyle -from botbuilder.dialogs.prompts import PromptOptions, ChoicePrompt -from botbuilder.dialogs.skills import SkillDialogOptions, SkillDialog, SkillDialogArgs -from botbuilder.core import ConversationState, MessageFactory, InvokeResponse -from botbuilder.core.skills import BotFrameworkSkill, ConversationIdFactoryBase +from botbuilder.core import MessageFactory, InvokeResponse from botbuilder.schema import Activity, ActivityTypes, InputHints -from botbuilder.integration.aiohttp.skills import SkillHttpClient -from ..config import DefaultConfig, SkillConfiguration -from ..dialogs import BookingDetails, BookingDialog, DialogSkillBotRecognizer, Location, OAuthTestDialog +from config import DefaultConfig +from dialogs import ( + DialogSkillBotRecognizer, + BookingDialog, + OAuthTestDialog, + BookingDetails, + Location, +) class ActivityRouterDialog(ComponentDialog): + """ + A root dialog that can route activities sent to the skill to different dialogs. + """ + def __init__( self, luis_recognizer: DialogSkillBotRecognizer, configuration: DefaultConfig ): @@ -50,9 +53,9 @@ async def process_activity( if current_activity_type == ActivityTypes.message: return await self._on_message_activity(step_context) - elif current_activity_type == ActivityTypes.invoke: + if current_activity_type == ActivityTypes.invoke: return await self._on_invoke_activity(step_context) - elif current_activity_type == ActivityTypes.event: + if current_activity_type == ActivityTypes.event: return await self._on_event_activity(step_context) # We didn't get an activity type we can handle. @@ -65,7 +68,7 @@ async def process_activity( return DialogTurnResult(DialogTurnStatus.Complete) async def _on_event_activity( - self, step_context: WaterfallStepContext + self, step_context: WaterfallStepContext ) -> DialogTurnResult: activity = step_context.context.activity @@ -78,22 +81,22 @@ async def _on_event_activity( # Start the booking dialog booking_dialog = self.find_dialog(BookingDialog.__name__) return await step_context.begin_dialog(booking_dialog.id, booking_details) - elif activity.name == "OAuthTest": + + if activity.name == "OAuthTest": # Start the oauth dialog oauth_dialog = self.find_dialog(OAuthTestDialog.__name__) return await step_context.begin_dialog(oauth_dialog.id, None) - else: - await step_context.context.send_activity( - MessageFactory.text( - f'Unrecognized EventName: "{activity.name}".', - input_hint=InputHints.ignoring_input, - ) + await step_context.context.send_activity( + MessageFactory.text( + f'Unrecognized EventName: "{activity.name}".', + input_hint=InputHints.ignoring_input, ) - return DialogTurnResult(DialogTurnStatus.Complete) + ) + return DialogTurnResult(DialogTurnStatus.Complete) async def _on_invoke_activity( - self, step_context: WaterfallStepContext + self, step_context: WaterfallStepContext ) -> DialogTurnResult: activity = step_context.context.activity @@ -104,7 +107,13 @@ async def _on_invoke_activity( location.from_json(activity.value) looking_into_it_message = "Getting your weather forecast..." - step_context.context.send_activity(MessageFactory.text(looking_into_it_message, looking_into_it_message, InputHints.ignoring_input)) + await step_context.context.send_activity( + MessageFactory.text( + looking_into_it_message, + looking_into_it_message, + InputHints.ignoring_input, + ) + ) # Create and return an invoke activity with the weather results. invoke_response_activity = Activity( @@ -112,14 +121,15 @@ async def _on_invoke_activity( value=InvokeResponse( body=[ "New York, NY, Clear, 56 F", - "Bellevue, WA, Mostly Cloudy, 48 F" + "Bellevue, WA, Mostly Cloudy, 48 F", ], - status=200 - ) + status=HTTPStatus.OK, + ), ) await step_context.context.send_activity(invoke_response_activity) else: + # We didn't get an invoke name we can handle. await step_context.context.send_activity( MessageFactory.text( f'Unrecognized InvokeName: "{activity.name}".', @@ -130,7 +140,7 @@ async def _on_invoke_activity( return DialogTurnResult(DialogTurnStatus.Complete) async def _on_message_activity( - self, step_context: WaterfallStepContext + self, step_context: WaterfallStepContext ) -> DialogTurnResult: activity = step_context.context.activity @@ -146,24 +156,22 @@ async def _on_message_activity( # Call LUIS with the utterance. luis_result = await self._luis_recognizer.recognize(step_context.context) - message = f"LUIS results for \"{activity.Text}\":\n" + message = f'LUIS results for "{activity.Text}":\n' intent, intent_score = None, None if luis_result.intents: - max_value_key = max(luis_result.intents, key=lambda key: luis_result.intents[key]) + max_value_key = max( + luis_result.intents, key=lambda key: luis_result.intents[key] + ) intent, intent_score = max_value_key, luis_result.intents[max_value_key] - message += f"Intent: \"{intent}\" Score: {intent_score}\n" + message += f'Intent: "{intent}" Score: {intent_score}\n' message += f"Entities found: {len(luis_result.entities) - 1}\n" for entity_key, entity_val in luis_result.entities: if not entity_key == "$instance": message += f"* {entity_val}\n" await step_context.context.send_activity( - MessageFactory.text( - message, - input_hint=InputHints.ignoring_input, - ) + MessageFactory.text(message, input_hint=InputHints.ignoring_input,) ) return DialogTurnResult(DialogTurnStatus.Complete) - diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py index 15c9e4ee9..538344764 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py @@ -1,11 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import platform - -from botbuilder.core import Bot, ConversationState, MessageFactory, TurnContext -from botbuilder.dialogs import Dialog, DialogContext, DialogSet, DialogTurnStatus -from botbuilder.schema import Activity, ActivityTypes, InputHints +from botbuilder.core import ( + Bot, + ConversationState, + TurnContext, +) +from botbuilder.dialogs import Dialog, DialogExtensions class SkillBot(Bot): @@ -14,51 +15,11 @@ def __init__(self, conversation_state: ConversationState, main_dialog: Dialog): self._main_dialog = main_dialog async def on_turn(self, context: TurnContext): - dialog_set = DialogSet(self._conversation_state.create_property("DialogState")) - dialog_set.add(self._main_dialog) - - dialog_context = await dialog_set.create_context(context) - if context.activity.type == ActivityTypes.end_of_conversation and dialog_context.stack: - # Handle remote cancellation request if we have something in the stack. - 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. - await active_dialog_context.cancel_all_dialogs() - remote_cancel_text = "**SkillBot.** The current mainDialog in the skill was **canceled** by a " \ - "request **from the host**, do some cleanup here if needed." - await context.send_activity(MessageFactory.text(remote_cancel_text, input_hint=InputHints.ignoring_input)) - else: - # Run the Dialog with the new message Activity and capture the results so we can send end of - # conversation if needed. - result = await dialog_context.continue_dialog() - if result.status == DialogTurnStatus.Empty: - start_message_text = f"**SkillBot.** Starting {self._main_dialog.id} " \ - f"(Pyhton {platform.python_version()})." - - await context.send_activity(MessageFactory.text( - start_message_text, - input_hint=InputHints.ignoring_input - )) - result = await dialog_context.begin_dialog(self._main_dialog.id) - - # Send end of conversation if it is complete - if result.status == DialogTurnStatus.Complete or result.status == DialogTurnStatus.Cancelled: - end_message_text = "**SkillBot.** The mainDialog in the skill has **completed**. " \ - "Sending EndOfConversation." - await context.send_activity(MessageFactory.text( - end_message_text, - input_hint=InputHints.ignoring_input - )) - - activity = Activity(type=ActivityTypes.end_of_conversation, value=result.result) - await context.send_activity(activity) - - # Save any state changes that might have happened during the turn. - await self._conversation_state.save_changes(context, False) - - - - def _get_active_dialog_context(self, dialog_context: DialogContext) -> DialogContext: - # Check non adaptive version of this method - raise NotImplementedError() + await DialogExtensions.run_dialog( + self._main_dialog, + context, + self._conversation_state.create_property("DialogState"), + ) + + # Save any state changes that might have occurred during the turn. + await self._conversation_state.save_changes(context) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py index 488a46631..a7b7236d0 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py @@ -4,14 +4,22 @@ import os -""" Bot Configuration """ - class DefaultConfig: """ Bot Configuration """ - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") + PORT = 39783 + APP_ID = os.environ.get("MicrosoftAppId", "67ec4e96-f4f6-424b-911f-de362a2a81d4") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "b0tframew0rks3cr3t!") CONNECTION_NAME = "" + + # If ALLOWED_CALLERS is empty, any bot can call this Skill. Add MicrosoftAppIds to restrict + # callers to only those specified. + # Example: os.environ.get("AllowedCallers", ["54d3bb6a-3b6d-4ccd-bbfd-cad5c72fb53a"]) + ALLOWED_CALLERS = os.environ.get("AllowedCallers", []) + + LUIS_APP_ID = os.environ.get("LuisAppId", "") + LUIS_API_KEY = os.environ.get("LuisAPIKey", "") + # LUIS endpoint host name, ie "westus.api.cognitive.microsoft.com" + LUIS_API_HOST_NAME = os.environ.get("LuisAPIHostName", "") diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/dialog_skill_bot_recognizer.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/dialog_skill_bot_recognizer.py index 6e2bb5018..6434fb13d 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/dialog_skill_bot_recognizer.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/dialog_skill_bot_recognizer.py @@ -4,7 +4,7 @@ from botbuilder.ai.luis import LuisApplication, LuisRecognizer from botbuilder.core import Recognizer, RecognizerResult, TurnContext -from ..config import DefaultConfig +from config import DefaultConfig class DialogSkillBotRecognizer(Recognizer): diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/oauth_test_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/oauth_test_dialog.py index d0258c357..53321bcd8 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/oauth_test_dialog.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/oauth_test_dialog.py @@ -15,8 +15,8 @@ from botbuilder.core import BotFrameworkAdapter, MessageFactory from botbuilder.schema import InputHints +from config import DefaultConfig from .cancel_and_help_dialog import CancelAndHelpDialog -from ..config import DefaultConfig class OAuthTestDialog(CancelAndHelpDialog): diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/requirements.txt b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/requirements.txt index 52eb5fe1e..353ad3a20 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/requirements.txt +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/requirements.txt @@ -1,2 +1,3 @@ aiohttp -botbuilder-core>=4.7.0 +botbuilder-core>=4.7.1 +datatypes-date-time>=1.0.0a2 From ed0817a8e8f0658794aff280b41fbb0cd17a5b75 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Mon, 9 Mar 2020 17:28:18 -0500 Subject: [PATCH 32/38] Corrected dialog-skill-bot AcitivyRouterDialog to actually have a WaterfallDialog step. --- .../dialog-skill-bot/bots/activity_router_dialog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py index 7bcaac203..75f22186a 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py @@ -36,7 +36,9 @@ def __init__( self.add_dialog(BookingDialog()) self.add_dialog(OAuthTestDialog(configuration)) - self.add_dialog(WaterfallDialog(WaterfallDialog.__name__, [None])) + self.add_dialog( + WaterfallDialog(WaterfallDialog.__name__, [self.process_activity]) + ) self.initial_dialog_id = WaterfallDialog.__name__ From c8c3528ff14d36cdc5b91f50bdc2ee272f07d8e8 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Tue, 10 Mar 2020 08:33:34 -0500 Subject: [PATCH 33/38] dialog-to-dialog test bot changes: dialog-echo-skill-bot, corrected missing async on ComponentDialog --- .../botbuilder/dialogs/component_dialog.py | 4 +-- .../botbuilder/dialogs/dialog_extensions.py | 4 +-- .../dialog-echo-skill-bot/app.py | 4 +-- .../authentication/__init__.py | 4 +-- .../allow_callers_claims_validation.py | 8 +++--- .../dialog-echo-skill-bot/bots/echo_bot.py | 26 +++++++++++++++---- .../dialog-echo-skill-bot/config.py | 7 +++-- .../skill_adapter_with_error_handler.py | 16 ++++++++---- .../skill_conversation_id_factory.py | 5 +++- .../bots/activity_router_dialog.py | 6 ++--- 10 files changed, 55 insertions(+), 29 deletions(-) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/component_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/component_dialog.py index bda4b711f..1034896b6 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/component_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/component_dialog.py @@ -189,7 +189,7 @@ def add_dialog(self, dialog: Dialog) -> object: self.initial_dialog_id = dialog.id return self - def find_dialog(self, dialog_id: str) -> Dialog: + async def find_dialog(self, dialog_id: str) -> Dialog: """ Finds a dialog by ID. @@ -197,7 +197,7 @@ def find_dialog(self, dialog_id: str) -> Dialog: :return: The dialog; or None if there is not a match for the ID. :rtype: :class:`botbuilder.dialogs.Dialog` """ - return self._dialogs.find(dialog_id) + return await self._dialogs.find(dialog_id) async def on_begin_dialog( self, inner_dc: DialogContext, options: object diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py index 96d8f1bdb..a6682dd13 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py @@ -36,10 +36,10 @@ async def run_dialog( # Process a reprompt event sent from the parent. if ( turn_context.activity.type == ActivityTypes.event - or turn_context.activity.name == DialogEvents.reprompt_dialog + and turn_context.activity.name == DialogEvents.reprompt_dialog and dialog_context.stack ): - await dialog.reprompt_dialog() + await dialog_context.reprompt_dialog() return # Run the Dialog with the new message Activity and capture the results diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py index c2e87e326..17a9410a1 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py @@ -7,7 +7,7 @@ BotFrameworkAdapterSettings, ConversationState, MemoryStorage, - UserState + UserState, ) from botbuilder.core.integration import aiohttp_error_middleware from botbuilder.schema import Activity @@ -31,7 +31,7 @@ SETTINGS = BotFrameworkAdapterSettings( CONFIG.APP_ID, CONFIG.APP_PASSWORD, - auth_configuration=AuthenticationConfiguration(claims_validator=VALIDATOR) + auth_configuration=AuthenticationConfiguration(claims_validator=VALIDATOR), ) ADAPTER = SkillAdapterWithErrorHandler(SETTINGS, CONVERSATION_STATE) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/__init__.py index 4ef47b33a..ebbe2ac15 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/__init__.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/__init__.py @@ -1,5 +1,3 @@ from .allow_callers_claims_validation import AllowedCallersClaimsValidator -__all__ = [ - "AllowedCallersClaimsValidator" -] \ No newline at end of file +__all__ = ["AllowedCallersClaimsValidator"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/allow_callers_claims_validation.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/allow_callers_claims_validation.py index 9da84cd1e..3403a32bc 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/allow_callers_claims_validation.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/allow_callers_claims_validation.py @@ -2,7 +2,7 @@ from botframework.connector.auth import JwtTokenValidation, SkillValidation -from ..config import DefaultConfig +from config import DefaultConfig class AllowedCallersClaimsValidator: @@ -14,12 +14,12 @@ def __init__(self, config: DefaultConfig): raise TypeError( "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 - self._allowed_callers = getattr(config) + self._allowed_callers = config.ALLOWED_CALLERS @property def claims_validator(self) -> Callable[[List[Dict]], Awaitable]: @@ -30,7 +30,7 @@ async def allow_callers_claims_validator(claims: Dict[str, object]): app_id = JwtTokenValidation.get_app_id_from_claims(claims) if app_id not in self._allowed_callers: raise PermissionError( - f"Received a request from a bot with an app ID of \"{app_id}\"." + f'Received a request from a bot with an app ID of "{app_id}".' f" To enable requests from this caller, add the app ID to your configuration file." ) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/echo_bot.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/echo_bot.py index 90a094640..89c37604b 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/echo_bot.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/echo_bot.py @@ -1,8 +1,13 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from botbuilder.core import ActivityHandler, MessageFactory, TurnContext -from botbuilder.schema import ChannelAccount +from botbuilder.core import ActivityHandler, TurnContext +from botbuilder.schema import ( + ChannelAccount, + Activity, + ActivityTypes, + EndOfConversationCodes, +) class EchoBot(ActivityHandler): @@ -14,6 +19,17 @@ async def on_members_added_activity( await turn_context.send_activity("Hello and welcome!") async def on_message_activity(self, turn_context: TurnContext): - return await turn_context.send_activity( - MessageFactory.text(f"Echo: {turn_context.activity.text}") - ) + if "end" in turn_context.activity.text or "stop" in turn_context.activity.text: + await turn_context.send_activity("ending conversation from the skill...") + end_of_conversation = Activity( + type=ActivityTypes.end_of_conversation, + code=EndOfConversationCodes.completed_successfully, + ) + await turn_context.send_activity(end_of_conversation) + else: + await turn_context.send_activity( + f"Echo: (Python) : {turn_context.activity.text}" + ) + await turn_context.send_activity( + 'Say "end" or "stop" and I\'ll end the conversation and back to the parent.' + ) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py index 4162719f0..56c4511b4 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py @@ -4,8 +4,6 @@ import os -""" Bot Configuration """ - class DefaultConfig: """ Bot Configuration """ @@ -13,3 +11,8 @@ class DefaultConfig: PORT = 39793 APP_ID = os.environ.get("MicrosoftAppId", "fb7a9f3c-2b30-4ac8-86a0-c44bdeaa380e") APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "b0tframew0rks3cr3t!") + + # If ALLOWED_CALLERS is empty, any bot can call this Skill. Add MicrosoftAppIds to restrict + # callers to only those specified. + # Example: os.environ.get("AllowedCallers", ["54d3bb6a-3b6d-4ccd-bbfd-cad5c72fb53a"]) + ALLOWED_CALLERS = os.environ.get("AllowedCallers", []) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/skill_adapter_with_error_handler.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/skill_adapter_with_error_handler.py index 0decd3d53..d7cf87c73 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/skill_adapter_with_error_handler.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/skill_adapter_with_error_handler.py @@ -33,11 +33,17 @@ async def on_error(context: TurnContext, error: Exception): # Send a message to the user error_message_text = "The skill encountered an error or bug." - error_message = MessageFactory.text(error_message_text, error_message_text, InputHints.ignoring_input) + error_message = MessageFactory.text( + error_message_text, error_message_text, InputHints.ignoring_input + ) await context.send_activity(error_message) - error_message_text = "To continue to run this bot, please fix the bot source code." - error_message = MessageFactory.text(error_message_text, error_message_text, InputHints.ignoring_input) + error_message_text = ( + "To continue to run this bot, please fix the bot source code." + ) + error_message = MessageFactory.text( + error_message_text, error_message_text, InputHints.ignoring_input + ) await context.send_activity(error_message) # Send a trace activity if we're talking to the Bot Framework Emulator @@ -62,7 +68,7 @@ async def on_error(context: TurnContext, error: Exception): except Exception as exception: print( f"\n Exception caught on attempting to Delete ConversationState : {exception}", - file=sys.stderr + file=sys.stderr, ) traceback.print_exc() @@ -80,7 +86,7 @@ async def on_error(context: TurnContext, error: Exception): "OnTurnError Trace", str(error), "https://www.botframework.com/schemas/error", - "TurnError" + "TurnError", ) self.on_turn_error = on_error diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py index 5d33d602e..f5eccc9ff 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py @@ -38,7 +38,10 @@ async def create_skill_conversation_id( conversation_reference = TurnContext.get_conversation_reference( options.activity ) - storage_key = f"{conversation_reference.channel_id}:{conversation_reference.conversation.id}" + storage_key = f"{conversation_reference.conversation.id}" \ + f"-{options.bot_framework_skill.id}" \ + f"-{conversation_reference.channel_id}" \ + f"-skillconvo" skill_conversation_reference = SkillConversationReference( conversation_reference=conversation_reference, diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py index 75f22186a..b9df3fbff 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py @@ -81,12 +81,12 @@ async def _on_event_activity( booking_details.from_json(activity.value) # Start the booking dialog - booking_dialog = self.find_dialog(BookingDialog.__name__) + booking_dialog = await self.find_dialog(BookingDialog.__name__) return await step_context.begin_dialog(booking_dialog.id, booking_details) if activity.name == "OAuthTest": # Start the oauth dialog - oauth_dialog = self.find_dialog(OAuthTestDialog.__name__) + oauth_dialog = await self.find_dialog(OAuthTestDialog.__name__) return await step_context.begin_dialog(oauth_dialog.id, None) await step_context.context.send_activity( @@ -150,7 +150,7 @@ async def _on_message_activity( await step_context.context.send_activity( MessageFactory.text( "NOTE: LUIS is not configured. To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and" - " 'LuisAPIHostName' to the appsettings.json file.", + " 'LuisAPIHostName' to the config.py file.", input_hint=InputHints.ignoring_input, ) ) From 6b137f73664944a9faf78c4c6a12cfdaaf41c192 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Tue, 10 Mar 2020 08:55:22 -0500 Subject: [PATCH 34/38] dialog-to-dialog: Handling messages with values (serialization and whatnot) --- .../dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py | 6 +++--- .../dialog-skill-bot/bots/activity_router_dialog.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py index 2dcffe3ee..953b076b2 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - +import json from typing import List from jsonpickle import encode @@ -267,7 +267,7 @@ def _create_dialog_skill_bot_activity(self, selected_option: str) -> Activity: attachments=[], entities=[], text=selected_option[:3].strip(), - value=BookingDetails(destination="New York"), + value=json.dumps(BookingDetails(destination="New York").__dict__), ) # Send an event activity to the skill with "OAuthTest" in the name. @@ -283,7 +283,7 @@ def _create_dialog_skill_bot_activity(self, selected_option: str) -> Activity: return Activity( type=ActivityTypes.event, name="BookFlight", - value=BookingDetails(destination="New York", origin="Seattle"), + value=json.dumps(BookingDetails(destination="New York", origin="Seattle").__dict__), ) raise Exception(f'Unable to create dialogArgs for "{selected_option}".') diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py index b9df3fbff..1df72b267 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json from http import HTTPStatus from botbuilder.dialogs import ( @@ -78,7 +79,7 @@ async def _on_event_activity( if activity.name == "BookFlight": booking_details = BookingDetails() if activity.value: - booking_details.from_json(activity.value) + booking_details.from_json(json.loads(activity.value)) # Start the booking dialog booking_dialog = await self.find_dialog(BookingDialog.__name__) @@ -106,7 +107,7 @@ async def _on_invoke_activity( if activity.name == "GetWeather": location = Location() if activity.value: - location.from_json(activity.value) + location.from_json(json.loads(activity.value)) looking_into_it_message = "Getting your weather forecast..." await step_context.context.send_activity( From f59e36f4900e74f8285d184c4814e33636fd6c27 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Tue, 10 Mar 2020 15:24:46 -0700 Subject: [PATCH 35/38] Memory storage does not validate e_tag integrity anymore, following the same behavior as C# --- .../botbuilder/core/memory_storage.py | 12 ++++++--- .../botbuilder/testing/storage_base_tests.py | 26 ++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/memory_storage.py b/libraries/botbuilder-core/botbuilder/core/memory_storage.py index 482527853..c61b053c7 100644 --- a/libraries/botbuilder-core/botbuilder/core/memory_storage.py +++ b/libraries/botbuilder-core/botbuilder/core/memory_storage.py @@ -73,10 +73,14 @@ async def write(self, changes: Dict[str, StoreItem]): "Etag conflict.\nOriginal: %s\r\nCurrent: %s" % (new_value_etag, old_state_etag) ) - if isinstance(new_state, dict): - new_state["e_tag"] = str(self._e_tag) - else: - new_state.e_tag = str(self._e_tag) + + # If the original object didn't have an e_tag, don't set one (C# behavior) + if old_state_etag: + if isinstance(new_state, dict): + new_state["e_tag"] = str(self._e_tag) + else: + new_state.e_tag = str(self._e_tag) + self._e_tag += 1 self.memory[key] = deepcopy(new_state) diff --git a/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py b/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py index afd17b905..51ff2fb04 100644 --- a/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py +++ b/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py @@ -94,8 +94,11 @@ async def create_object(storage) -> bool: store_items["createPocoStoreItem"]["id"] == read_store_items["createPocoStoreItem"]["id"] ) + """ + If decided to validate e_tag integrity aagain, uncomment this code assert read_store_items["createPoco"]["e_tag"] is not None assert read_store_items["createPocoStoreItem"]["e_tag"] is not None + """ return True @@ -127,9 +130,9 @@ async def update_object(storage) -> bool: loaded_store_items = await storage.read(["pocoItem", "pocoStoreItem"]) update_poco_item = loaded_store_items["pocoItem"] - update_poco_item["e_tag"] = None + # update_poco_item["e_tag"] = None update_poco_store_item = loaded_store_items["pocoStoreItem"] - assert update_poco_store_item["e_tag"] is not None + # assert update_poco_store_item["e_tag"] is not None # 2nd write should work update_poco_item["count"] += 1 @@ -142,10 +145,6 @@ async def update_object(storage) -> bool: reloaded_update_poco_item = reloaded_store_items["pocoItem"] reloaded_update_poco_store_item = reloaded_store_items["pocoStoreItem"] - assert reloaded_update_poco_item["e_tag"] is not None - assert ( - update_poco_store_item["e_tag"] != reloaded_update_poco_store_item["e_tag"] - ) assert reloaded_update_poco_item["count"] == 2 assert reloaded_update_poco_store_item["count"] == 2 @@ -153,17 +152,20 @@ async def update_object(storage) -> bool: update_poco_item["count"] = 123 await storage.write({"pocoItem": update_poco_item}) + """ + If decided to validate e_tag integrity aagain, uncomment this code # Write with old eTag should FAIL for storeItem update_poco_store_item["count"] = 123 with pytest.raises(Exception) as err: await storage.write({"pocoStoreItem": update_poco_store_item}) assert err.value is not None + """ reloaded_store_items2 = await storage.read(["pocoItem", "pocoStoreItem"]) reloaded_poco_item2 = reloaded_store_items2["pocoItem"] - reloaded_poco_item2["e_tag"] = None + # reloaded_poco_item2["e_tag"] = None reloaded_poco_store_item2 = reloaded_store_items2["pocoStoreItem"] assert reloaded_poco_item2["count"] == 123 @@ -172,7 +174,7 @@ async def update_object(storage) -> bool: # write with wildcard etag should work reloaded_poco_item2["count"] = 100 reloaded_poco_store_item2["count"] = 100 - reloaded_poco_store_item2["e_tag"] = "*" + # reloaded_poco_store_item2["e_tag"] = "*" wildcard_etag_dict = { "pocoItem": reloaded_poco_item2, @@ -192,12 +194,15 @@ async def update_object(storage) -> bool: assert reloaded_store_item4 is not None + """ + If decided to validate e_tag integrity aagain, uncomment this code reloaded_store_item4["e_tag"] = "" dict2 = {"pocoStoreItem": reloaded_store_item4} with pytest.raises(Exception) as err: await storage.write(dict2) assert err.value is not None + """ final_store_items = await storage.read(["pocoItem", "pocoStoreItem"]) assert final_store_items["pocoItem"]["count"] == 100 @@ -213,7 +218,7 @@ async def delete_object(storage) -> bool: read_store_items = await storage.read(["delete1"]) - assert read_store_items["delete1"]["e_tag"] + # assert read_store_items["delete1"]["e_tag"] assert read_store_items["delete1"]["count"] == 1 await storage.delete(["delete1"]) @@ -248,9 +253,12 @@ async def perform_batch_operations(storage) -> bool: assert result["batch1"]["count"] == 10 assert result["batch2"]["count"] == 20 assert result["batch3"]["count"] == 30 + """ + If decided to validate e_tag integrity aagain, uncomment this code assert result["batch1"].get("e_tag", None) is not None assert result["batch2"].get("e_tag", None) is not None assert result["batch3"].get("e_tag", None) is not None + """ await storage.delete(["batch1", "batch2", "batch3"]) From edc2b02e14caf7927031833859deebf959724db5 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Tue, 10 Mar 2020 16:24:27 -0700 Subject: [PATCH 36/38] pylint: Memory storage does not validate e_tag integrity anymore, following the same behavior as C# --- .../botbuilder-testing/botbuilder/testing/storage_base_tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py b/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py index 51ff2fb04..96044f9de 100644 --- a/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py +++ b/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py @@ -38,6 +38,7 @@ async def test_handle_null_keys_when_reading(self): class StorageBaseTests: + # pylint: disable=pointless-string-statement @staticmethod async def return_empty_object_when_reading_unknown_key(storage) -> bool: result = await storage.read(["unknown"]) From dda49359f11d05ca8787ce907366943f7eaebd97 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Tue, 10 Mar 2020 17:41:19 -0700 Subject: [PATCH 37/38] pylint: Memory storage does not validate e_tag integrity anymore, following the same behavior as C# --- .../dialog-echo-skill-bot/config.py | 4 +- .../adapter_with_error_handler.py | 117 ++++++++++++------ .../dialog-root-bot/config.py | 8 +- .../dialog-skill-bot/config.py | 4 +- 4 files changed, 87 insertions(+), 46 deletions(-) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py index 56c4511b4..500488e6b 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py @@ -9,8 +9,8 @@ class DefaultConfig: """ Bot Configuration """ PORT = 39793 - APP_ID = os.environ.get("MicrosoftAppId", "fb7a9f3c-2b30-4ac8-86a0-c44bdeaa380e") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "b0tframew0rks3cr3t!") + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") # If ALLOWED_CALLERS is empty, any bot can call this Skill. Add MicrosoftAppIds to restrict # callers to only those specified. diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py index 9ae38e44a..400ade905 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py @@ -11,37 +11,52 @@ MessageFactory, TurnContext, ) +from botbuilder.integration.aiohttp.skills import SkillHttpClient from botbuilder.schema import ActivityTypes, Activity, InputHints +from config import DefaultConfig, SkillConfiguration +from dialogs import MainDialog class AdapterWithErrorHandler(BotFrameworkAdapter): def __init__( self, settings: BotFrameworkAdapterSettings, + config: DefaultConfig, conversation_state: ConversationState = None, + skill_client: SkillHttpClient = None, + skill_config: SkillConfiguration = None ): super().__init__(settings) + self._config = config self._conversation_state = conversation_state + self._skill_client = skill_client + self._skill_config = skill_config - # Catch-all for errors. - async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - traceback.print_exc() + self.on_turn_error = self._handle_turn_error + async def _handle_turn_error(self, turn_context: TurnContext, error: Exception): + # This check writes out errors to console log + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() + await self._send_error_message(turn_context, error) + await self._end_skill_conversation(turn_context, error) + await self._clear_conversation_state(turn_context) + + async def _send_error_message(self, turn_context: TurnContext, error: Exception): + try: # Send a message to the user error_message_text = "The skill encountered an error or bug." error_message = MessageFactory.text(error_message_text, error_message_text, InputHints.ignoring_input) - await context.send_activity(error_message) + await turn_context.send_activity(error_message) error_message_text = "To continue to run this bot, please fix the bot source code." error_message = MessageFactory.text(error_message_text, error_message_text, InputHints.ignoring_input) - await context.send_activity(error_message) + await turn_context.send_activity(error_message) # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": + if turn_context.activity.channel_id == "emulator": # Create a trace activity that contains the error object trace_activity = Activity( label="TurnError", @@ -52,35 +67,61 @@ async def on_error(context: TurnContext, error: Exception): value_type="https://www.botframework.com/schemas/error", ) # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - # Clear out state - nonlocal self - if self._conversation_state: - try: - await self._conversation_state.delete(context) - except Exception as exception: - print( - f"\n Exception caught on attempting to Delete ConversationState : {exception}", - file=sys.stderr - ) - traceback.print_exc() + await turn_context.send_activity(trace_activity) + except Exception as exception: + print( + f"\n Exception caught on _send_error_message : {exception}", + file=sys.stderr + ) + traceback.print_exc() + + async def _end_skill_conversation(self, turn_context: TurnContext, error: Exception): + if not self._conversation_state or not self._skill_client or not self._skill_config: + return + + try: + # Inform the active skill that the conversation is ended so that it has + # a chance to clean up. + # Note: ActiveSkillPropertyName is set by the RooBot while messages are being + # forwarded to a Skill. + active_skill = await self._conversation_state.create_property( + MainDialog.ACTIVE_SKILL_PROPERTY_NAME + ) - # Send and EndOfConversation activity to the skill caller with the error to end the conversation - # and let the caller decide what to do. - end_of_conversation = Activity(type=ActivityTypes.end_of_conversation) - end_of_conversation.code = "SkillError" - end_of_conversation.text = str(error) - await context.send_activity(end_of_conversation) + if active_skill: + bot_id = self._config.APP_ID + end_of_conversation = Activity(type=ActivityTypes.end_of_conversation) + end_of_conversation.code = "RootSkillError" + TurnContext.apply_conversation_reference( + end_of_conversation, + TurnContext.get_conversation_reference(turn_context.activity), + True + ) - # Send a trace activity, which will be displayed in the Bot Framework Emulator - # Note: we return the entire exception in the value property to help the developer, - # this should not be done in prod. - await context.send_trace_activity( - "OnTurnError Trace", - str(error), - "https://www.botframework.com/schemas/error", - "TurnError" + await self._conversation_state.save_changes(turn_context, True) + await self._skill_client.post_activity_to_skill( + bot_id, + active_skill, + self._skill_config.SKILL_HOST_ENDPOINT, + end_of_conversation + ) + except Exception as exception: + print( + f"\n Exception caught on _end_skill_conversation : {exception}", + file=sys.stderr ) + traceback.print_exc() - self.on_turn_error = on_error + async def _clear_conversation_state(self, turn_context: TurnContext): + if self._conversation_state: + try: + # Delete the conversationState for the current conversation to prevent the + # bot from getting stuck in a error-loop caused by being in a bad state. + # ConversationState should be thought of as similar to "cookie-state" in a Web pages. + await self._conversation_state.delete(turn_context) + except Exception as exception: + print( + f"\n Exception caught on _clear_conversation_state : {exception}", + file=sys.stderr + ) + traceback.print_exc() diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py index 1bf1201ad..000d3e481 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py @@ -11,18 +11,18 @@ class DefaultConfig: """ Bot Configuration """ PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "b56a1b59-7081-4546-b3fa-177401fd0657") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "b0tframew0rks3cr3t!") + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") SKILL_HOST_ENDPOINT = "http://localhost:3978/api/skills" SKILLS = [ { "id": "EchoSkillBot", - "app_id": "fb7a9f3c-2b30-4ac8-86a0-c44bdeaa380e", + "app_id": "", "skill_endpoint": "http://localhost:39793/api/messages", }, { "id": "DialogSkillBot", - "app_id": "67ec4e96-f4f6-424b-911f-de362a2a81d4", + "app_id": "", "skill_endpoint": "http://localhost:39783/api/messages", }, ] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py index a7b7236d0..31662c36b 100644 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py +++ b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py @@ -9,8 +9,8 @@ class DefaultConfig: """ Bot Configuration """ PORT = 39783 - APP_ID = os.environ.get("MicrosoftAppId", "67ec4e96-f4f6-424b-911f-de362a2a81d4") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "b0tframew0rks3cr3t!") + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") CONNECTION_NAME = "" From c31fbd216717fe6b134bed77faf3e9daf378a432 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Tue, 10 Mar 2020 17:43:43 -0700 Subject: [PATCH 38/38] Removing samples from product code PR --- .../dialog-echo-skill-bot/README.md | 38 --- .../dialog-echo-skill-bot/app.py | 66 ---- .../authentication/__init__.py | 3 - .../allow_callers_claims_validation.py | 39 --- .../dialog-echo-skill-bot/bots/__init__.py | 6 - .../dialog-echo-skill-bot/bots/echo_bot.py | 35 --- .../dialog-echo-skill-bot/config.py | 18 -- .../template-with-new-rg.json | 264 ---------------- .../template-with-preexisting-rg.json | 242 --------------- .../dialog-echo-skill-bot/requirements.txt | 2 - .../skill_adapter_with_error_handler.py | 92 ------ .../adapter_with_error_handler.py | 127 -------- .../dialog-to-dialog/dialog-root-bot/app.py | 87 ------ .../authentication/__init__.py | 3 - .../allowed_skills_claims_validator.py | 30 -- .../dialog-root-bot/bots/__init__.py | 4 - .../dialog-root-bot/bots/root_bot.py | 60 ---- .../dialog-root-bot/cards/welcomeCard.json | 46 --- .../dialog-root-bot/config.py | 35 --- .../dialog-root-bot/dialogs/__init__.py | 11 - .../dialogs/booking_details.py | 18 -- .../dialog-root-bot/dialogs/location.py | 11 - .../dialog-root-bot/dialogs/main_dialog.py | 289 ------------------ .../dialog-root-bot/dialogs/tangent_dialog.py | 39 --- .../dialog-root-bot/helpers/__init__.py | 6 - .../helpers/activity_helper.py | 37 --- .../dialog-root-bot/helpers/dialog_helper.py | 19 -- .../dialog-root-bot/helpers/luis_helper.py | 102 ------- .../dialog-root-bot/middleware/__init__.py | 4 - .../middleware/dummy_middleware.py | 32 -- .../skill_conversation_id_factory.py | 68 ----- .../dialog-to-dialog/dialog-skill-bot/app.py | 69 ----- .../authentication/__init__.py | 3 - .../allow_callers_claims_validation.py | 39 --- .../dialog-skill-bot/bots/__init__.py | 7 - .../bots/activity_router_dialog.py | 180 ----------- .../dialog-skill-bot/bots/skill_bot.py | 25 -- .../dialog-skill-bot/config.py | 25 -- .../template-with-new-rg.json | 264 ---------------- .../template-with-preexisting-rg.json | 242 --------------- .../dialog-skill-bot/dialogs/__init__.py | 18 -- .../dialogs/booking_details.py | 24 -- .../dialogs/booking_dialog.py | 136 --------- .../dialogs/cancel_and_help_dialog.py | 47 --- .../dialogs/date_resolver_dialog.py | 82 ----- .../dialogs/dialog_skill_bot_recognizer.py | 34 --- .../dialog-skill-bot/dialogs/location.py | 16 - .../dialogs/oauth_test_dialog.py | 89 ------ .../dialog-skill-bot/requirements.txt | 3 - .../skill_adapter_with_error_handler.py | 92 ------ 50 files changed, 3228 deletions(-) delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/README.md delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/__init__.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/allow_callers_claims_validation.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/__init__.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/echo_bot.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-new-rg.json delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-preexisting-rg.json delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/requirements.txt delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/skill_adapter_with_error_handler.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/__init__.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allowed_skills_claims_validator.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/__init__.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/cards/welcomeCard.json delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/__init__.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/booking_details.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/location.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/tangent_dialog.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/__init__.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/activity_helper.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/dialog_helper.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/luis_helper.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/__init__.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/dummy_middleware.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/app.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/__init__.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/allow_callers_claims_validation.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/__init__.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-new-rg.json delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-preexisting-rg.json delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/__init__.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_details.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_dialog.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/cancel_and_help_dialog.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/date_resolver_dialog.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/dialog_skill_bot_recognizer.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/location.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/oauth_test_dialog.py delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/requirements.txt delete mode 100644 samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/skill_adapter_with_error_handler.py diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/README.md b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/README.md deleted file mode 100644 index 299259d5b..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# EchoBot - -Bot Framework v4 echo bot sample. - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-samples.git -``` -- Bring up a terminal, navigate to `botbuilder-samples\samples\python\02.echo-bot` folder -- Activate your desired virtual environment -- In the terminal, type `pip install -r requirements.txt` -- Run your bot with `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- Paste this URL in the emulator window - http://localhost:3978/api/messages - -## Deploy the bot to Azure - -To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions. - -# Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) -- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) -- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) -- [Azure Portal](https://portal.azure.com) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py deleted file mode 100644 index 17a9410a1..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/app.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from aiohttp import web -from aiohttp.web import Request, Response, json_response -from botbuilder.core import ( - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - UserState, -) -from botbuilder.core.integration import aiohttp_error_middleware -from botbuilder.schema import Activity -from botframework.connector.auth import AuthenticationConfiguration - -from authentication import AllowedCallersClaimsValidator -from bots import EchoBot -from config import DefaultConfig -from skill_adapter_with_error_handler import SkillAdapterWithErrorHandler - -CONFIG = DefaultConfig() - -# Create MemoryStorage, UserState and ConversationState -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) -CONVERSATION_STATE = ConversationState(MEMORY) - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -VALIDATOR = AllowedCallersClaimsValidator(CONFIG).claims_validator -SETTINGS = BotFrameworkAdapterSettings( - CONFIG.APP_ID, - CONFIG.APP_PASSWORD, - auth_configuration=AuthenticationConfiguration(claims_validator=VALIDATOR), -) -ADAPTER = SkillAdapterWithErrorHandler(SETTINGS, CONVERSATION_STATE) - -# Create the Bot -BOT = EchoBot() - - -# Listen for incoming requests on /api/messages -async def messages(req: Request) -> Response: - # Main bot message handler. - if "application/json" in req.headers["Content-Type"]: - body = await req.json() - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = req.headers["Authorization"] if "Authorization" in req.headers else "" - - response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - if response: - return json_response(data=response.body, status=response.status) - return Response(status=201) - - -APP = web.Application(middlewares=[aiohttp_error_middleware]) -APP.router.add_post("/api/messages", messages) - -if __name__ == "__main__": - try: - web.run_app(APP, host="localhost", port=CONFIG.PORT) - except Exception as error: - raise error diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/__init__.py deleted file mode 100644 index ebbe2ac15..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .allow_callers_claims_validation import AllowedCallersClaimsValidator - -__all__ = ["AllowedCallersClaimsValidator"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/allow_callers_claims_validation.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/allow_callers_claims_validation.py deleted file mode 100644 index 3403a32bc..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/authentication/allow_callers_claims_validation.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import Awaitable, Callable, Dict, List - -from botframework.connector.auth import JwtTokenValidation, SkillValidation - -from config import DefaultConfig - - -class AllowedCallersClaimsValidator: - - config_key = "ALLOWED_CALLERS" - - def __init__(self, config: DefaultConfig): - if not config: - raise TypeError( - "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 - self._allowed_callers = config.ALLOWED_CALLERS - - @property - def claims_validator(self) -> Callable[[List[Dict]], Awaitable]: - async def allow_callers_claims_validator(claims: Dict[str, object]): - # if _allowedCallers is None we allow all calls - if 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. - app_id = JwtTokenValidation.get_app_id_from_claims(claims) - if app_id not in self._allowed_callers: - raise PermissionError( - f'Received a request from a bot with an app ID of "{app_id}".' - f" To enable requests from this caller, add the app ID to your configuration file." - ) - - return - - return allow_callers_claims_validator diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/__init__.py deleted file mode 100644 index f95fbbbad..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .echo_bot import EchoBot - -__all__ = ["EchoBot"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/echo_bot.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/echo_bot.py deleted file mode 100644 index 89c37604b..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/bots/echo_bot.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ActivityHandler, TurnContext -from botbuilder.schema import ( - ChannelAccount, - Activity, - ActivityTypes, - EndOfConversationCodes, -) - - -class EchoBot(ActivityHandler): - async def on_members_added_activity( - self, members_added: [ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity("Hello and welcome!") - - async def on_message_activity(self, turn_context: TurnContext): - if "end" in turn_context.activity.text or "stop" in turn_context.activity.text: - await turn_context.send_activity("ending conversation from the skill...") - end_of_conversation = Activity( - type=ActivityTypes.end_of_conversation, - code=EndOfConversationCodes.completed_successfully, - ) - await turn_context.send_activity(end_of_conversation) - else: - await turn_context.send_activity( - f"Echo: (Python) : {turn_context.activity.text}" - ) - await turn_context.send_activity( - 'Say "end" or "stop" and I\'ll end the conversation and back to the parent.' - ) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py deleted file mode 100644 index 500488e6b..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/config.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 39793 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") - - # If ALLOWED_CALLERS is empty, any bot can call this Skill. Add MicrosoftAppIds to restrict - # callers to only those specified. - # Example: os.environ.get("AllowedCallers", ["54d3bb6a-3b6d-4ccd-bbfd-cad5c72fb53a"]) - ALLOWED_CALLERS = os.environ.get("AllowedCallers", []) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-new-rg.json b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-new-rg.json deleted file mode 100644 index b68e51ec6..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-new-rg.json +++ /dev/null @@ -1,264 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "groupLocation": { - "type": "string", - "metadata": { - "description": "Specifies the location of the Resource Group." - } - }, - "groupName": { - "type": "string", - "metadata": { - "description": "Specifies the name of the Resource Group." - } - }, - "appId": { - "type": "string", - "metadata": { - "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." - } - }, - "appSecret": { - "type": "string", - "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." - } - }, - "botId": { - "type": "string", - "metadata": { - "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." - } - }, - "botSku": { - "type": "string", - "metadata": { - "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." - } - }, - "newAppServicePlanName": { - "type": "string", - "metadata": { - "description": "The name of the App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "newAppServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan. Defaults to \"westus\"." - } - }, - "newWebAppName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." - } - } - }, - "variables": { - "appServicePlanName": "[parameters('newAppServicePlanName')]", - "resourcesLocation": "[parameters('newAppServicePlanLocation')]", - "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", - "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", - "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]", - "publishingUsername": "[concat('$', parameters('newWebAppName'))]" - }, - "resources": [ - { - "name": "[parameters('groupName')]", - "type": "Microsoft.Resources/resourceGroups", - "apiVersion": "2018-05-01", - "location": "[parameters('groupLocation')]", - "properties": { - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2018-05-01", - "name": "storageDeployment", - "resourceGroup": "[parameters('groupName')]", - "dependsOn": [ - "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" - ], - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": {}, - "variables": {}, - "resources": [ - { - "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", - "type": "Microsoft.Web/serverfarms", - "name": "[variables('appServicePlanName')]", - "apiVersion": "2018-02-01", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "kind": "linux", - "properties": { - "name": "[variables('appServicePlanName')]", - "perSiteScaling": false, - "reserved": true, - "targetWorkerCount": 0, - "targetWorkerSizeId": 0 - } - }, - { - "comments": "Create a Web App using a Linux App Service Plan", - "type": "Microsoft.Web/sites", - "apiVersion": "2015-08-01", - "location": "[variables('resourcesLocation')]", - "kind": "app,linux", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" - ], - "name": "[variables('webAppName')]", - "properties": { - "name": "[variables('webAppName')]", - "hostNameSslStates": [ - { - "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Standard" - }, - { - "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Repository" - } - ], - "serverFarmId": "[variables('appServicePlanName')]", - "siteConfig": { - "appSettings": [ - { - "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", - "value": "true" - }, - { - "name": "MicrosoftAppId", - "value": "[parameters('appId')]" - }, - { - "name": "MicrosoftAppPassword", - "value": "[parameters('appSecret')]" - } - ], - "cors": { - "allowedOrigins": [ - "https://botservice.hosting.portal.azure.net", - "https://hosting.onecloud.azure-test.net/" - ] - } - } - } - }, - { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2016-08-01", - "name": "[concat(variables('webAppName'), '/web')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ], - "properties": { - "numberOfWorkers": 1, - "defaultDocuments": [ - "Default.htm", - "Default.html", - "Default.asp", - "index.htm", - "index.html", - "iisstart.htm", - "default.aspx", - "index.php", - "hostingstart.html" - ], - "netFrameworkVersion": "v4.0", - "phpVersion": "", - "pythonVersion": "", - "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", - "requestTracingEnabled": false, - "remoteDebuggingEnabled": false, - "remoteDebuggingVersion": "VS2017", - "httpLoggingEnabled": true, - "logsDirectorySizeLimit": 35, - "detailedErrorLoggingEnabled": false, - "publishingUsername": "[variables('publishingUsername')]", - "scmType": "None", - "use32BitWorkerProcess": true, - "webSocketsEnabled": false, - "alwaysOn": false, - "appCommandLine": "gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:APP", - "managedPipelineMode": "Integrated", - "virtualApplications": [ - { - "virtualPath": "/", - "physicalPath": "site\\wwwroot", - "preloadEnabled": false, - "virtualDirectories": null - } - ], - "winAuthAdminState": 0, - "winAuthTenantState": 0, - "customAppPoolIdentityAdminState": false, - "customAppPoolIdentityTenantState": false, - "loadBalancing": "LeastRequests", - "routingRules": [], - "experiments": { - "rampUpRules": [] - }, - "autoHealEnabled": false, - "vnetName": "", - "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", - "reservedInstanceCount": 0 - } - }, - { - "apiVersion": "2017-12-01", - "type": "Microsoft.BotService/botServices", - "name": "[parameters('botId')]", - "location": "global", - "kind": "bot", - "sku": { - "name": "[parameters('botSku')]" - }, - "properties": { - "name": "[parameters('botId')]", - "displayName": "[parameters('botId')]", - "endpoint": "[variables('botEndpoint')]", - "msaAppId": "[parameters('appId')]", - "developerAppInsightsApplicationId": null, - "developerAppInsightKey": null, - "publishingCredentials": null, - "storageResourceId": null - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" - ] - } - ], - "outputs": {} - } - } - } - ] -} \ No newline at end of file diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-preexisting-rg.json b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-preexisting-rg.json deleted file mode 100644 index 66b044b52..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/deploymentTemplates/template-with-preexisting-rg.json +++ /dev/null @@ -1,242 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appId": { - "type": "string", - "metadata": { - "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." - } - }, - "appSecret": { - "type": "string", - "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." - } - }, - "botId": { - "type": "string", - "metadata": { - "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." - } - }, - "botSku": { - "defaultValue": "F0", - "type": "string", - "metadata": { - "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." - } - }, - "newAppServicePlanName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The name of the new App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "appServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan." - } - }, - "existingAppServicePlan": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the existing App Service Plan used to create the Web App for the bot." - } - }, - "newWebAppName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." - } - } - }, - "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", - "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", - "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", - "publishingUsername": "[concat('$', parameters('newWebAppName'))]", - "resourcesLocation": "[parameters('appServicePlanLocation')]", - "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", - "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", - "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" - }, - "resources": [ - { - "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2016-09-01", - "name": "[variables('servicePlanName')]", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "kind": "linux", - "properties": { - "name": "[variables('servicePlanName')]", - "perSiteScaling": false, - "reserved": true, - "targetWorkerCount": 0, - "targetWorkerSizeId": 0 - } - }, - { - "comments": "Create a Web App using a Linux App Service Plan", - "type": "Microsoft.Web/sites", - "apiVersion": "2016-08-01", - "name": "[variables('webAppName')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" - ], - "kind": "app,linux", - "properties": { - "enabled": true, - "hostNameSslStates": [ - { - "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Standard" - }, - { - "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Repository" - } - ], - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", - "reserved": true, - "scmSiteAlsoStopped": false, - "clientAffinityEnabled": false, - "clientCertEnabled": false, - "hostNamesDisabled": false, - "containerSize": 0, - "dailyMemoryTimeQuota": 0, - "httpsOnly": false, - "siteConfig": { - "appSettings": [ - { - "name": "MicrosoftAppId", - "value": "[parameters('appId')]" - }, - { - "name": "MicrosoftAppPassword", - "value": "[parameters('appSecret')]" - }, - { - "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", - "value": "true" - } - ], - "cors": { - "allowedOrigins": [ - "https://botservice.hosting.portal.azure.net", - "https://hosting.onecloud.azure-test.net/" - ] - } - } - } - }, - { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2016-08-01", - "name": "[concat(variables('webAppName'), '/web')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ], - "properties": { - "numberOfWorkers": 1, - "defaultDocuments": [ - "Default.htm", - "Default.html", - "Default.asp", - "index.htm", - "index.html", - "iisstart.htm", - "default.aspx", - "index.php", - "hostingstart.html" - ], - "netFrameworkVersion": "v4.0", - "phpVersion": "", - "pythonVersion": "", - "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", - "requestTracingEnabled": false, - "remoteDebuggingEnabled": false, - "remoteDebuggingVersion": "VS2017", - "httpLoggingEnabled": true, - "logsDirectorySizeLimit": 35, - "detailedErrorLoggingEnabled": false, - "publishingUsername": "[variables('publishingUsername')]", - "scmType": "None", - "use32BitWorkerProcess": true, - "webSocketsEnabled": false, - "alwaysOn": false, - "appCommandLine": "gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:APP", - "managedPipelineMode": "Integrated", - "virtualApplications": [ - { - "virtualPath": "/", - "physicalPath": "site\\wwwroot", - "preloadEnabled": false, - "virtualDirectories": null - } - ], - "winAuthAdminState": 0, - "winAuthTenantState": 0, - "customAppPoolIdentityAdminState": false, - "customAppPoolIdentityTenantState": false, - "loadBalancing": "LeastRequests", - "routingRules": [], - "experiments": { - "rampUpRules": [] - }, - "autoHealEnabled": false, - "vnetName": "", - "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", - "reservedInstanceCount": 0 - } - }, - { - "apiVersion": "2017-12-01", - "type": "Microsoft.BotService/botServices", - "name": "[parameters('botId')]", - "location": "global", - "kind": "bot", - "sku": { - "name": "[parameters('botSku')]" - }, - "properties": { - "name": "[parameters('botId')]", - "displayName": "[parameters('botId')]", - "endpoint": "[variables('botEndpoint')]", - "msaAppId": "[parameters('appId')]", - "developerAppInsightsApplicationId": null, - "developerAppInsightKey": null, - "publishingCredentials": null, - "storageResourceId": null - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" - ] - } - ] -} \ No newline at end of file diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/requirements.txt b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/requirements.txt deleted file mode 100644 index 52eb5fe1e..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -aiohttp -botbuilder-core>=4.7.0 diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/skill_adapter_with_error_handler.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/skill_adapter_with_error_handler.py deleted file mode 100644 index d7cf87c73..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-echo-skill-bot/skill_adapter_with_error_handler.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import sys -import traceback -from datetime import datetime - -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MessageFactory, - TurnContext, -) -from botbuilder.schema import ActivityTypes, Activity, InputHints - - -class SkillAdapterWithErrorHandler(BotFrameworkAdapter): - def __init__( - self, - settings: BotFrameworkAdapterSettings, - conversation_state: ConversationState = None, - ): - super().__init__(settings) - self._conversation_state = conversation_state - - # Catch-all for errors. - async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - traceback.print_exc() - - # Send a message to the user - error_message_text = "The skill encountered an error or bug." - error_message = MessageFactory.text( - error_message_text, error_message_text, InputHints.ignoring_input - ) - await context.send_activity(error_message) - - error_message_text = ( - "To continue to run this bot, please fix the bot source code." - ) - error_message = MessageFactory.text( - error_message_text, error_message_text, InputHints.ignoring_input - ) - await context.send_activity(error_message) - - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - # Clear out state - nonlocal self - if self._conversation_state: - try: - await self._conversation_state.delete(context) - except Exception as exception: - print( - f"\n Exception caught on attempting to Delete ConversationState : {exception}", - file=sys.stderr, - ) - traceback.print_exc() - - # Send and EndOfConversation activity to the skill caller with the error to end the conversation - # and let the caller decide what to do. - end_of_conversation = Activity(type=ActivityTypes.end_of_conversation) - end_of_conversation.code = "SkillError" - end_of_conversation.text = str(error) - await context.send_activity(end_of_conversation) - - # Send a trace activity, which will be displayed in the Bot Framework Emulator - # Note: we return the entire exception in the value property to help the developer, - # this should not be done in prod. - await context.send_trace_activity( - "OnTurnError Trace", - str(error), - "https://www.botframework.com/schemas/error", - "TurnError", - ) - - self.on_turn_error = on_error diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py deleted file mode 100644 index 400ade905..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/adapter_with_error_handler.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import sys -import traceback -from datetime import datetime - -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MessageFactory, - TurnContext, -) -from botbuilder.integration.aiohttp.skills import SkillHttpClient -from botbuilder.schema import ActivityTypes, Activity, InputHints - -from config import DefaultConfig, SkillConfiguration -from dialogs import MainDialog - -class AdapterWithErrorHandler(BotFrameworkAdapter): - def __init__( - self, - settings: BotFrameworkAdapterSettings, - config: DefaultConfig, - conversation_state: ConversationState = None, - skill_client: SkillHttpClient = None, - skill_config: SkillConfiguration = None - ): - super().__init__(settings) - self._config = config - self._conversation_state = conversation_state - self._skill_client = skill_client - self._skill_config = skill_config - - self.on_turn_error = self._handle_turn_error - - async def _handle_turn_error(self, turn_context: TurnContext, error: Exception): - # This check writes out errors to console log - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - traceback.print_exc() - await self._send_error_message(turn_context, error) - await self._end_skill_conversation(turn_context, error) - await self._clear_conversation_state(turn_context) - - async def _send_error_message(self, turn_context: TurnContext, error: Exception): - try: - # Send a message to the user - error_message_text = "The skill encountered an error or bug." - error_message = MessageFactory.text(error_message_text, error_message_text, InputHints.ignoring_input) - await turn_context.send_activity(error_message) - - error_message_text = "To continue to run this bot, please fix the bot source code." - error_message = MessageFactory.text(error_message_text, error_message_text, InputHints.ignoring_input) - await turn_context.send_activity(error_message) - - # Send a trace activity if we're talking to the Bot Framework Emulator - if turn_context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await turn_context.send_activity(trace_activity) - except Exception as exception: - print( - f"\n Exception caught on _send_error_message : {exception}", - file=sys.stderr - ) - traceback.print_exc() - - async def _end_skill_conversation(self, turn_context: TurnContext, error: Exception): - if not self._conversation_state or not self._skill_client or not self._skill_config: - return - - try: - # Inform the active skill that the conversation is ended so that it has - # a chance to clean up. - # Note: ActiveSkillPropertyName is set by the RooBot while messages are being - # forwarded to a Skill. - active_skill = await self._conversation_state.create_property( - MainDialog.ACTIVE_SKILL_PROPERTY_NAME - ) - - if active_skill: - bot_id = self._config.APP_ID - end_of_conversation = Activity(type=ActivityTypes.end_of_conversation) - end_of_conversation.code = "RootSkillError" - TurnContext.apply_conversation_reference( - end_of_conversation, - TurnContext.get_conversation_reference(turn_context.activity), - True - ) - - await self._conversation_state.save_changes(turn_context, True) - await self._skill_client.post_activity_to_skill( - bot_id, - active_skill, - self._skill_config.SKILL_HOST_ENDPOINT, - end_of_conversation - ) - except Exception as exception: - print( - f"\n Exception caught on _end_skill_conversation : {exception}", - file=sys.stderr - ) - traceback.print_exc() - - async def _clear_conversation_state(self, turn_context: TurnContext): - if self._conversation_state: - try: - # Delete the conversationState for the current conversation to prevent the - # bot from getting stuck in a error-loop caused by being in a bad state. - # ConversationState should be thought of as similar to "cookie-state" in a Web pages. - await self._conversation_state.delete(turn_context) - except Exception as exception: - print( - f"\n Exception caught on _clear_conversation_state : {exception}", - file=sys.stderr - ) - traceback.print_exc() diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py deleted file mode 100644 index 037e5da25..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/app.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from aiohttp import web -from aiohttp.web import Request, Response -from botbuilder.core import ( - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - UserState, -) -from botbuilder.core.integration import ( - aiohttp_channel_service_routes, - aiohttp_error_middleware, -) -from botbuilder.core.skills import SkillHandler -from botbuilder.schema import Activity -from botbuilder.integration.aiohttp.skills import SkillHttpClient -from botframework.connector.auth import ( - AuthenticationConfiguration, - SimpleCredentialProvider, -) - -from authentication import AllowedSkillsClaimsValidator -from bots import RootBot -from dialogs import MainDialog -from config import DefaultConfig, SkillConfiguration -from adapter_with_error_handler import AdapterWithErrorHandler -from skill_conversation_id_factory import SkillConversationIdFactory - -CONFIG = DefaultConfig() -SKILL_CONFIG = SkillConfiguration() - -# Create MemoryStorage, UserState and ConversationState -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) -CONVERSATION_STATE = ConversationState(MEMORY) -ID_FACTORY = SkillConversationIdFactory(MEMORY) - -CREDENTIAL_PROVIDER = SimpleCredentialProvider(CONFIG.APP_ID, CONFIG.APP_PASSWORD) -CLIENT = SkillHttpClient(CREDENTIAL_PROVIDER, ID_FACTORY) - - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD) -ADAPTER = AdapterWithErrorHandler(SETTINGS, CONVERSATION_STATE) - -DIALOG = MainDialog(CONVERSATION_STATE, ID_FACTORY, CLIENT, SKILL_CONFIG, CONFIG) - -# Create the Bot -BOT = RootBot(CONVERSATION_STATE, DIALOG) # , SKILL_CONFIG, ID_FACTORY, CLIENT, CONFIG) - -AUTH_CONFIG = AuthenticationConfiguration( - claims_validator=AllowedSkillsClaimsValidator(CONFIG).validate_claims -) - -SKILL_HANDLER = SkillHandler(ADAPTER, BOT, ID_FACTORY, CREDENTIAL_PROVIDER, AUTH_CONFIG) - - -# Listen for incoming requests on /api/messages -async def messages(req: Request) -> Response: - # Main bot message handler. - if "application/json" in req.headers["Content-Type"]: - body = await req.json() - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = req.headers["Authorization"] if "Authorization" in req.headers else "" - - try: - await ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - return Response(status=201) - except Exception as exception: - raise exception - - -APP = web.Application(middlewares=[aiohttp_error_middleware]) -APP.router.add_post("/api/messages", messages) -APP.router.add_routes(aiohttp_channel_service_routes(SKILL_HANDLER, "/api/skills")) - -if __name__ == "__main__": - try: - web.run_app(APP, host="localhost", port=CONFIG.PORT) - except Exception as error: - raise error diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/__init__.py deleted file mode 100644 index f60882d12..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .allowed_skills_claims_validator import AllowedSkillsClaimsValidator - -__all__ = ["AllowedSkillsClaimsValidator"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allowed_skills_claims_validator.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allowed_skills_claims_validator.py deleted file mode 100644 index 55b0af43d..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/authentication/allowed_skills_claims_validator.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botframework.connector.auth import JwtTokenValidation, SkillValidation - -from config import DefaultConfig - - -class AllowedSkillsClaimsValidator: - """ - Sample claims validator that loads an allowed list from config if present and checks - that requests are coming from allowed skills. - """ - - def __init__(self, config: DefaultConfig): - self.allowed_callers = config.SKILLS - - # Check AppIds for the configured callers (we will only allow responses from skills we have configured). - # SkillConfiguration.SKILLS is the list of Skill app Ids that are allowed to access the parent. - # To add a new skill simply go to the config.py file and add - # the skill's id, Microsoft AppId and skill_endpoint to the array under SKILLS. - async def validate_claims(self, claims: dict): - if SkillValidation.is_skill_claim(claims) and self.allowed_callers: - # Check that the appId claim in the request is in the list of skills configured for this bot. - app_id = JwtTokenValidation.get_app_id_from_claims(claims) - if app_id not in self.allowed_callers: - raise ValueError( - f'Received a request from an application with an appID of "{ app_id }". To enable requests ' - f"from this skill, add the id to your configuration file." - ) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/__init__.py deleted file mode 100644 index be7e157a7..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .root_bot import RootBot - - -__all__ = ["RootBot"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py deleted file mode 100644 index 402fe128d..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/bots/root_bot.py +++ /dev/null @@ -1,60 +0,0 @@ -import json -import os.path - -from typing import List - -from botbuilder.core import ( - ActivityHandler, - ConversationState, - MessageFactory, - TurnContext, -) -from botbuilder.dialogs import Dialog, DialogExtensions -from botbuilder.schema import ActivityTypes, Attachment, ChannelAccount - - -class RootBot(ActivityHandler): - def __init__( - self, conversation_state: ConversationState, main_dialog: Dialog, - ): - self._conversation_state = conversation_state - self._main_dialog = main_dialog - - 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 - await DialogExtensions.run_dialog( - self._main_dialog, - turn_context, - self._conversation_state.create_property("DialogState"), - ) - else: - await super().on_turn(turn_context) - - # Save any state changes that might have occurred during the turn. - await self._conversation_state.save_changes(turn_context) - - async def on_members_added_activity( - self, members_added: List[ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - if member.id != turn_context.activity.recipient.id: - welcome_card = self._create_adaptive_card_attachment() - activity = MessageFactory.attachment(welcome_card) - await turn_context.send_activity(activity) - await DialogExtensions.run_dialog( - self._main_dialog, - turn_context, - self._conversation_state.create_property("DialogState"), - ) - - def _create_adaptive_card_attachment(self) -> Attachment: - relative_path = os.path.abspath(os.path.dirname(__file__)) - path = os.path.join(relative_path, "../cards/welcomeCard.json") - with open(path) as in_file: - card = json.load(in_file) - - return Attachment( - content_type="application/vnd.microsoft.card.adaptive", content=card - ) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/cards/welcomeCard.json b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/cards/welcomeCard.json deleted file mode 100644 index cc10cda9f..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/cards/welcomeCard.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "type": "AdaptiveCard", - "version": "1.0", - "body": [ - { - "type": "Image", - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU", - "size": "stretch" - }, - { - "type": "TextBlock", - "spacing": "medium", - "size": "default", - "weight": "bolder", - "text": "Welcome to Bot Framework!", - "wrap": true, - "maxLines": 0 - }, - { - "type": "TextBlock", - "size": "default", - "isSubtle": "true", - "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.", - "wrap": true, - "maxLines": 0 - } - ], - "actions": [ - { - "type": "Action.OpenUrl", - "title": "Get an overview", - "url": "https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0" - }, - { - "type": "Action.OpenUrl", - "title": "Ask a question", - "url": "https://stackoverflow.com/questions/tagged/botframework" - }, - { - "type": "Action.OpenUrl", - "title": "Learn how to deploy", - "url": "https://docs.microsoft.com/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0" - } - ] -} \ No newline at end of file diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py deleted file mode 100644 index 000d3e481..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/config.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -from typing import Dict -from botbuilder.core.skills import BotFrameworkSkill - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") - SKILL_HOST_ENDPOINT = "http://localhost:3978/api/skills" - SKILLS = [ - { - "id": "EchoSkillBot", - "app_id": "", - "skill_endpoint": "http://localhost:39793/api/messages", - }, - { - "id": "DialogSkillBot", - "app_id": "", - "skill_endpoint": "http://localhost:39783/api/messages", - }, - ] - - -class SkillConfiguration: - SKILL_HOST_ENDPOINT = DefaultConfig.SKILL_HOST_ENDPOINT - SKILLS: Dict[str, BotFrameworkSkill] = { - skill["id"]: BotFrameworkSkill(**skill) for skill in DefaultConfig.SKILLS - } diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/__init__.py deleted file mode 100644 index d490c7656..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from .booking_details import BookingDetails -from .location import Location -from .main_dialog import MainDialog -from .tangent_dialog import TangentDialog - -__all__ = [ - "BookingDetails", - "Location", - "MainDialog", - "TangentDialog" -] \ No newline at end of file diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/booking_details.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/booking_details.py deleted file mode 100644 index db7c4c60c..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/booking_details.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class BookingDetails: - def __init__( - self, - destination: str = None, - origin: str = None, - travel_date: str = None, - unsupported_airports=None, - ): - if unsupported_airports is None: - unsupported_airports = [] - self.destination = destination - self.origin = origin - self.travel_date = travel_date - self.unsupported_airports = unsupported_airports diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/location.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/location.py deleted file mode 100644 index 045aaedfa..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/location.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class Location: - def __init__( - self, latitude: str = None, longitude: str = None, postal_code: str = None, - ): - self.latitude = latitude - self.longitude = longitude - self.postal_code = postal_code diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py deleted file mode 100644 index 953b076b2..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/main_dialog.py +++ /dev/null @@ -1,289 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -from typing import List - -from jsonpickle import encode -from botbuilder.dialogs import ( - ComponentDialog, - DialogContext, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, -) -from botbuilder.dialogs.choices import Choice, ListStyle -from botbuilder.dialogs.prompts import PromptOptions, ChoicePrompt -from botbuilder.dialogs.skills import ( - SkillDialogOptions, - SkillDialog, - BeginSkillDialogOptions, -) -from botbuilder.core import ConversationState, MessageFactory -from botbuilder.core.skills import BotFrameworkSkill, ConversationIdFactoryBase -from botbuilder.schema import Activity, ActivityTypes, InputHints, DeliveryModes -from botbuilder.integration.aiohttp.skills import SkillHttpClient - -from config import SkillConfiguration, DefaultConfig -from .booking_details import BookingDetails -from .tangent_dialog import TangentDialog - - -class MainDialog(ComponentDialog): - - ACTIVE_SKILL_PROPERTY_NAME = f"MainDialog.ActiveSkillProperty" - - def __init__( - self, - conversation_state: ConversationState, - conversation_id_factory: ConversationIdFactoryBase, - skill_client: SkillHttpClient, - skills_config: SkillConfiguration, - configuration: DefaultConfig, - ): - super(MainDialog, self).__init__(MainDialog.__name__) - - self._selected_skill_key = ( - f"{MainDialog.__module__}.{MainDialog.__name__}.SelectedSkillKey" - ) - - bot_id = configuration.APP_ID - if not bot_id: - raise TypeError("App Id is not in configuration") - - self._skills_config = skills_config - if not self._skills_config: - raise TypeError("Skills configuration cannot be None") - - if not skill_client: - raise TypeError("skill_client cannot be None") - - if not conversation_state: - raise TypeError("conversation_state cannot be None") - - # ChoicePrompt to render available skills and skill actions - self.add_dialog(ChoicePrompt(ChoicePrompt.__name__)) - - # Register the tangent - self.add_dialog(TangentDialog()) - - # Create SkillDialog instances for the configured skills - for _, skill_info in skills_config.SKILLS.items(): - # SkillDialog used to wrap interaction with the selected skill - skill_dialog_options = SkillDialogOptions( - bot_id=bot_id, - conversation_id_factory=conversation_id_factory, - skill_client=skill_client, - skill=skill_info, - skill_host_endpoint=skills_config.SKILL_HOST_ENDPOINT, - conversation_state=conversation_state, - ) - - self.add_dialog(SkillDialog(skill_dialog_options, skill_info.id)) - - # Main waterfall dialog for this bot - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, - [ - self.select_skill_step, - self.select_skill_action_step, - self.call_skill_action_step, - self.final_step, - ], - ) - ) - - self._active_skill_property = conversation_state.create_property( - MainDialog.ACTIVE_SKILL_PROPERTY_NAME - ) - - 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 - active_skill = await self._active_skill_property.get(inner_dc.context) - activity = inner_dc.context.activity - - if ( - active_skill - and activity.type == ActivityTypes.message - and "abort" in activity.text - ): - # Cancel all dialog when the user says abort. - await inner_dc.cancel_all_dialogs() - return await inner_dc.replace_dialog(self.initial_dialog_id) - - if ( - active_skill - and activity.type == ActivityTypes.message - and "tangent" in activity.text - ): - # Begin Tangent - return await inner_dc.replace_dialog(TangentDialog.__name__) - - return await super().on_continue_dialog(inner_dc) - - async def select_skill_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - # Create the PromptOptions from the skill configuration which contain the list of configured skills. - options = PromptOptions( - prompt=MessageFactory.text("What skill would you like to call?"), - retry_prompt=MessageFactory.text( - "That was not a valid choice, please select a valid skill." - ), - choices=[ - Choice(value=skill.id) - for _, skill in self._skills_config.SKILLS.items() - ], - ) - - # Prompt the user to select a skill. - return await step_context.prompt(ChoicePrompt.__name__, options) - - async def select_skill_action_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - # Get the skill info based on the selected skill. - selected_skill_id = step_context.result - selected_skill = self._skills_config.SKILLS.get(selected_skill_id.value) - - # Remember the skill selected by the user. - step_context.values[self._selected_skill_key] = selected_skill - - # Create the PromptOptions with the actions supported by the selected skill. - options = PromptOptions( - prompt=MessageFactory.text( - f"What action would you like to call in **{selected_skill.id}**?" - ), - retry_prompt=MessageFactory.text( - "That was not a valid choice, please select a valid action." - ), - choices=self._get_skill_actions(selected_skill), - style=ListStyle.suggested_action, - ) - - # Prompt the user to select a skill action. - return await step_context.prompt(ChoicePrompt.__name__, options) - - async def call_skill_action_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - # Starts SkillDialog based on the user's selections - selected_skill: BotFrameworkSkill = step_context.values[ - self._selected_skill_key - ] - - if selected_skill.id == "EchoSkillBot": - # Echo skill only handles message activities, send a dummy utterance to get it started. - skill_activity = Activity( - type=ActivityTypes.message, - attachments=[], - entities=[], - text="Start echo skill", - ) - elif selected_skill.id == "DialogSkillBot": - skill_activity = self._create_dialog_skill_bot_activity( - step_context.result.value - ) - else: - raise Exception(f"Unknown target skill id: {selected_skill.id}.") - - skill_dialog_args = BeginSkillDialogOptions(skill_activity) - - # We are manually creating the activity to send to the skill, ensure we add the ChannelData and Properties - # from the original activity so the skill gets them. - # Note: this is not necessary if we are just forwarding the current activity from context. - skill_dialog_args.activity.channel_data = ( - step_context.context.activity.channel_data - ) - skill_dialog_args.activity.additional_properties = ( - step_context.context.activity.additional_properties - ) - - skill_dialog_args.activity.delivery_mode = DeliveryModes.expect_replies - - await self._active_skill_property.set(step_context.context, selected_skill) - - return await step_context.begin_dialog(selected_skill.id, skill_dialog_args) - - async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - active_skill = await self._active_skill_property.get(step_context.context) - - if step_context.result: - message = "Skill invocation complete." - message += f" Result: {encode(step_context.result)}" - await step_context.context.send_activity( - MessageFactory.text(message, input_hint=InputHints.ignoring_input) - ) - - # Clear the skill selected by the user. - step_context.values[self._selected_skill_key] = None - - # 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 - return await step_context.replace_dialog(self.initial_dialog_id) - - # Helper method to create Choice elements for the actions supported by the skill - def _get_skill_actions(self, skill: BotFrameworkSkill) -> List[Choice]: - # Note: the bot would probably render this by readying the skill manifest - # we are just using hardcoded skill actions here for simplicity. - - choices = [] - if skill.id == "EchoSkillBot": - choices.append(Choice("Messages")) - - elif skill.id == "DialogSkillBot": - choices.append(Choice("m:some message for tomorrow")) - choices.append(Choice("BookFlight")) - choices.append(Choice("OAuthTest")) - choices.append(Choice("mv:some message with value")) - choices.append(Choice("BookFlightWithValues")) - - return choices - - # Helper method to create the activity to be sent to the DialogSkillBot - def _create_dialog_skill_bot_activity(self, selected_option: str) -> Activity: - # Note: in a real bot, the dialogArgs will be created dynamically based on the conversation - # and what each action requires, this code hardcodes the values to make things simpler. - - selected_option = selected_option.lower() - - # Send a message activity to the skill. - if selected_option.startswith("m:"): - return Activity( - type=ActivityTypes.message, - attachments=[], - entities=[], - text=selected_option[:2].strip(), - ) - - # Send a message activity to the skill with some artificial parameters in value - elif selected_option.startswith("mv:"): - return Activity( - type=ActivityTypes.message, - attachments=[], - entities=[], - text=selected_option[:3].strip(), - value=json.dumps(BookingDetails(destination="New York").__dict__), - ) - - # Send an event activity to the skill with "OAuthTest" in the name. - elif selected_option == "oauthtest": - return Activity(type=ActivityTypes.event, name="OAuthTest") - - # Send an event activity to the skill with "BookFlight" in the name. - elif selected_option == "bookflight": - return Activity(type=ActivityTypes.event, name="BookFlight") - - # Send an event activity to the skill "BookFlight" in the name and some testing values. - elif selected_option == "bookflightwithvalues": - return Activity( - type=ActivityTypes.event, - name="BookFlight", - value=json.dumps(BookingDetails(destination="New York", origin="Seattle").__dict__), - ) - - raise Exception(f'Unable to create dialogArgs for "{selected_option}".') diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/tangent_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/tangent_dialog.py deleted file mode 100644 index a0d300788..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/dialogs/tangent_dialog.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import MessageFactory -from botbuilder.dialogs import ComponentDialog, DialogTurnResult, WaterfallDialog, WaterfallStepContext -from botbuilder.dialogs.prompts import TextPrompt, PromptOptions -from botbuilder.schema import InputHints - - -class TangentDialog(ComponentDialog): - def __init__(self, dialog_id: str = "TangentDialog"): - super().__init__(dialog_id) - - self.add_dialog(TextPrompt(TextPrompt.__name__)) - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, - [ - self.step_1, - self.step_2 - ], - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def step_1( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - prompt_message = MessageFactory.text("Tangent step 1 of 2", InputHints.expecting_input) - - return await step_context.prompt(TextPrompt.__name__, PromptOptions(prompt=prompt_message)) - - async def step_2( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - prompt_message = MessageFactory.text("Tangent step 2 of 2", InputHints.expecting_input) - - return await step_context.prompt(TextPrompt.__name__, PromptOptions(prompt=prompt_message)) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/__init__.py deleted file mode 100644 index 699f8693c..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from . import activity_helper, luis_helper, dialog_helper - -__all__ = ["activity_helper", "dialog_helper", "luis_helper"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/activity_helper.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/activity_helper.py deleted file mode 100644 index 29a24823e..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/activity_helper.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from datetime import datetime -from botbuilder.schema import ( - Activity, - ActivityTypes, - ChannelAccount, - ConversationAccount, -) - - -def create_activity_reply(activity: Activity, text: str = None, locale: str = None): - - return Activity( - type=ActivityTypes.message, - timestamp=datetime.utcnow(), - from_property=ChannelAccount( - id=getattr(activity.recipient, "id", None), - name=getattr(activity.recipient, "name", None), - ), - recipient=ChannelAccount( - id=activity.from_property.id, name=activity.from_property.name - ), - reply_to_id=activity.id, - service_url=activity.service_url, - channel_id=activity.channel_id, - conversation=ConversationAccount( - is_group=activity.conversation.is_group, - id=activity.conversation.id, - name=activity.conversation.name, - ), - text=text or "", - locale=locale or "", - attachments=[], - entities=[], - ) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/dialog_helper.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/dialog_helper.py deleted file mode 100644 index 6b2646b0b..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/dialog_helper.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import StatePropertyAccessor, TurnContext -from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus - - -class DialogHelper: - @staticmethod - async def run_dialog( - dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor - ): - dialog_set = DialogSet(accessor) - dialog_set.add(dialog) - - dialog_context = await dialog_set.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog(dialog.id) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/luis_helper.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/luis_helper.py deleted file mode 100644 index 3e28bc47e..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/helpers/luis_helper.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from enum import Enum -from typing import Dict -from botbuilder.ai.luis import LuisRecognizer -from botbuilder.core import IntentScore, TopIntent, TurnContext - -from booking_details import BookingDetails - - -class Intent(Enum): - BOOK_FLIGHT = "BookFlight" - CANCEL = "Cancel" - GET_WEATHER = "GetWeather" - NONE_INTENT = "NoneIntent" - - -def top_intent(intents: Dict[Intent, dict]) -> TopIntent: - max_intent = Intent.NONE_INTENT - max_value = 0.0 - - for intent, value in intents: - intent_score = IntentScore(value) - if intent_score.score > max_value: - max_intent, max_value = intent, intent_score.score - - return TopIntent(max_intent, max_value) - - -class LuisHelper: - @staticmethod - async def execute_luis_query( - luis_recognizer: LuisRecognizer, turn_context: TurnContext - ) -> (Intent, object): - """ - Returns an object with preformatted LUIS results for the bot's dialogs to consume. - """ - result = None - intent = None - - try: - recognizer_result = await luis_recognizer.recognize(turn_context) - - intent = ( - sorted( - recognizer_result.intents, - key=recognizer_result.intents.get, - reverse=True, - )[:1][0] - if recognizer_result.intents - else None - ) - - if intent == Intent.BOOK_FLIGHT.value: - result = BookingDetails() - - # We need to get the result from the LUIS JSON which at every level returns an array. - to_entities = recognizer_result.entities.get("$instance", {}).get( - "To", [] - ) - if len(to_entities) > 0: - if recognizer_result.entities.get("To", [{"$instance": {}}])[0][ - "$instance" - ]: - result.destination = to_entities[0]["text"].capitalize() - else: - result.unsupported_airports.append( - to_entities[0]["text"].capitalize() - ) - - from_entities = recognizer_result.entities.get("$instance", {}).get( - "From", [] - ) - if len(from_entities) > 0: - if recognizer_result.entities.get("From", [{"$instance": {}}])[0][ - "$instance" - ]: - result.origin = from_entities[0]["text"].capitalize() - else: - result.unsupported_airports.append( - from_entities[0]["text"].capitalize() - ) - - # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop - # the Time part. TIMEX is a format that represents DateTime expressions that include some ambiguity. - # e.g. missing a Year. - date_entities = recognizer_result.entities.get("datetime", []) - if date_entities: - timex = date_entities[0]["timex"] - - if timex: - datetime = timex[0].split("T")[0] - - result.travel_date = datetime - - else: - result.travel_date = None - - except Exception as exception: - print(exception) - - return intent, result diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/__init__.py deleted file mode 100644 index c23b52ce2..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .dummy_middleware import DummyMiddleware - - -__all__ = ["DummyMiddleware"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/dummy_middleware.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/dummy_middleware.py deleted file mode 100644 index 4d38fe79f..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/middleware/dummy_middleware.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Awaitable, Callable, List - -from botbuilder.core import Middleware, TurnContext -from botbuilder.schema import Activity, ResourceResponse - - -class DummyMiddleware(Middleware): - def __init__(self, label: str): - self._label = label - - async def on_turn( - self, context: TurnContext, logic: Callable[[TurnContext], Awaitable] - ): - message = f"{self._label} {context.activity.type} {context.activity.text}" - print(message) - - # Register outgoing handler - context.on_send_activities(self._outgoing_handler) - - await logic() - - async def _outgoing_handler( - self, - context: TurnContext, # pylint: disable=unused-argument - activities: List[Activity], - logic: Callable[[TurnContext], Awaitable[List[ResourceResponse]]], - ): - for activity in activities: - message = f"{self._label} {activity.type} {activity.text}" - print(message) - - return await logic() diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py deleted file mode 100644 index f5eccc9ff..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-root-bot/skill_conversation_id_factory.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from typing import Union - -from botbuilder.core import Storage, TurnContext -from botbuilder.core.skills import ( - ConversationIdFactoryBase, - SkillConversationIdFactoryOptions, - SkillConversationReference, -) -from botbuilder.schema import ConversationReference - - -class SkillConversationIdFactory(ConversationIdFactoryBase): - def __init__(self, storage: Storage): - if not storage: - raise TypeError("storage can't be None") - - self._storage = storage - - async def create_skill_conversation_id( - self, - options_or_conversation_reference: Union[ - SkillConversationIdFactoryOptions, ConversationReference - ], - ) -> str: - if not options_or_conversation_reference: - raise TypeError("Need options or conversation reference") - - if not isinstance( - options_or_conversation_reference, SkillConversationIdFactoryOptions - ): - raise TypeError( - "This SkillConversationIdFactory can only handle SkillConversationIdFactoryOptions" - ) - - options = options_or_conversation_reference - conversation_reference = TurnContext.get_conversation_reference( - options.activity - ) - storage_key = f"{conversation_reference.conversation.id}" \ - f"-{options.bot_framework_skill.id}" \ - f"-{conversation_reference.channel_id}" \ - f"-skillconvo" - - skill_conversation_reference = SkillConversationReference( - conversation_reference=conversation_reference, - oauth_scope=options.from_bot_oauth_scope, - ) - - skill_conversation_info = {storage_key: skill_conversation_reference} - - await self._storage.write(skill_conversation_info) - - return storage_key - - async def get_conversation_reference( - self, skill_conversation_id: str - ) -> Union[SkillConversationReference, ConversationReference]: - if not skill_conversation_id: - raise TypeError("skill_conversation_id can't be None") - - skill_conversation_info = await self._storage.read([skill_conversation_id]) - - return skill_conversation_info.get(skill_conversation_id) - - async def delete_conversation_reference(self, skill_conversation_id: str): - await self._storage.delete([skill_conversation_id]) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/app.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/app.py deleted file mode 100644 index a877a4e11..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/app.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from aiohttp import web -from aiohttp.web import Request, Response, json_response -from botbuilder.core import ( - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - UserState, -) -from botbuilder.core.integration import aiohttp_error_middleware -from botbuilder.schema import Activity -from botframework.connector.auth import AuthenticationConfiguration - -from authentication import AllowedCallersClaimsValidator -from bots import SkillBot, ActivityRouterDialog -from config import DefaultConfig -from dialogs import DialogSkillBotRecognizer -from skill_adapter_with_error_handler import SkillAdapterWithErrorHandler - -CONFIG = DefaultConfig() - -# Create MemoryStorage, UserState and ConversationState -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) -CONVERSATION_STATE = ConversationState(MEMORY) - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -VALIDATOR = AllowedCallersClaimsValidator(CONFIG).claims_validator -SETTINGS = BotFrameworkAdapterSettings( - CONFIG.APP_ID, - CONFIG.APP_PASSWORD, - auth_configuration=AuthenticationConfiguration(claims_validator=VALIDATOR), -) -ADAPTER = SkillAdapterWithErrorHandler(SETTINGS, CONVERSATION_STATE) - -# Create the Bot -RECOGNIZER = DialogSkillBotRecognizer(CONFIG) -ROUTER = ActivityRouterDialog(RECOGNIZER, CONFIG) -BOT = SkillBot(CONVERSATION_STATE, ROUTER) - - -# Listen for incoming requests on /api/messages -async def messages(req: Request) -> Response: - # Main bot message handler. - if "application/json" in req.headers["Content-Type"]: - body = await req.json() - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = req.headers["Authorization"] if "Authorization" in req.headers else "" - - response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - if response: - return json_response(data=response.body, status=response.status) - return Response(status=201) - - -APP = web.Application(middlewares=[aiohttp_error_middleware]) -APP.router.add_post("/api/messages", messages) - -if __name__ == "__main__": - try: - web.run_app(APP, host="localhost", port=CONFIG.PORT) - except Exception as error: - raise error diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/__init__.py deleted file mode 100644 index ebbe2ac15..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .allow_callers_claims_validation import AllowedCallersClaimsValidator - -__all__ = ["AllowedCallersClaimsValidator"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/allow_callers_claims_validation.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/allow_callers_claims_validation.py deleted file mode 100644 index 3403a32bc..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/authentication/allow_callers_claims_validation.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import Awaitable, Callable, Dict, List - -from botframework.connector.auth import JwtTokenValidation, SkillValidation - -from config import DefaultConfig - - -class AllowedCallersClaimsValidator: - - config_key = "ALLOWED_CALLERS" - - def __init__(self, config: DefaultConfig): - if not config: - raise TypeError( - "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 - self._allowed_callers = config.ALLOWED_CALLERS - - @property - def claims_validator(self) -> Callable[[List[Dict]], Awaitable]: - async def allow_callers_claims_validator(claims: Dict[str, object]): - # if _allowedCallers is None we allow all calls - if 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. - app_id = JwtTokenValidation.get_app_id_from_claims(claims) - if app_id not in self._allowed_callers: - raise PermissionError( - f'Received a request from a bot with an app ID of "{app_id}".' - f" To enable requests from this caller, add the app ID to your configuration file." - ) - - return - - return allow_callers_claims_validator diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/__init__.py deleted file mode 100644 index 4d7e002ab..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .skill_bot import SkillBot -from .activity_router_dialog import ActivityRouterDialog - -__all__ = ["SkillBot", "ActivityRouterDialog"] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py deleted file mode 100644 index 1df72b267..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/activity_router_dialog.py +++ /dev/null @@ -1,180 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -from http import HTTPStatus - -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, - DialogTurnStatus, -) -from botbuilder.core import MessageFactory, InvokeResponse -from botbuilder.schema import Activity, ActivityTypes, InputHints - -from config import DefaultConfig -from dialogs import ( - DialogSkillBotRecognizer, - BookingDialog, - OAuthTestDialog, - BookingDetails, - Location, -) - - -class ActivityRouterDialog(ComponentDialog): - """ - A root dialog that can route activities sent to the skill to different dialogs. - """ - - def __init__( - self, luis_recognizer: DialogSkillBotRecognizer, configuration: DefaultConfig - ): - super().__init__(ActivityRouterDialog.__name__) - - self._luis_recognizer = luis_recognizer - - self.add_dialog(BookingDialog()) - self.add_dialog(OAuthTestDialog(configuration)) - self.add_dialog( - WaterfallDialog(WaterfallDialog.__name__, [self.process_activity]) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def process_activity( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - current_activity_type = step_context.context.activity.type - - # A skill can send trace activities if needed - await step_context.context.send_trace_activity( - f"{ActivityRouterDialog.__name__}.process_activity()", - label=f"Got ActivityType: {current_activity_type}", - ) - - if current_activity_type == ActivityTypes.message: - return await self._on_message_activity(step_context) - if current_activity_type == ActivityTypes.invoke: - return await self._on_invoke_activity(step_context) - if current_activity_type == ActivityTypes.event: - return await self._on_event_activity(step_context) - - # We didn't get an activity type we can handle. - await step_context.context.send_activity( - MessageFactory.text( - f'Unrecognized ActivityType: "{current_activity_type}".', - input_hint=InputHints.ignoring_input, - ) - ) - return DialogTurnResult(DialogTurnStatus.Complete) - - async def _on_event_activity( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - activity = step_context.context.activity - - # Resolve what to execute based on the event name. - if activity.name == "BookFlight": - booking_details = BookingDetails() - if activity.value: - booking_details.from_json(json.loads(activity.value)) - - # Start the booking dialog - booking_dialog = await self.find_dialog(BookingDialog.__name__) - return await step_context.begin_dialog(booking_dialog.id, booking_details) - - if activity.name == "OAuthTest": - # Start the oauth dialog - oauth_dialog = await self.find_dialog(OAuthTestDialog.__name__) - return await step_context.begin_dialog(oauth_dialog.id, None) - - await step_context.context.send_activity( - MessageFactory.text( - f'Unrecognized EventName: "{activity.name}".', - input_hint=InputHints.ignoring_input, - ) - ) - return DialogTurnResult(DialogTurnStatus.Complete) - - async def _on_invoke_activity( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - activity = step_context.context.activity - - # Resolve what to execute based on the event name. - if activity.name == "GetWeather": - location = Location() - if activity.value: - location.from_json(json.loads(activity.value)) - - looking_into_it_message = "Getting your weather forecast..." - await step_context.context.send_activity( - MessageFactory.text( - looking_into_it_message, - looking_into_it_message, - InputHints.ignoring_input, - ) - ) - - # Create and return an invoke activity with the weather results. - invoke_response_activity = Activity( - type="invokeResponse", - value=InvokeResponse( - body=[ - "New York, NY, Clear, 56 F", - "Bellevue, WA, Mostly Cloudy, 48 F", - ], - status=HTTPStatus.OK, - ), - ) - - await step_context.context.send_activity(invoke_response_activity) - else: - # We didn't get an invoke name we can handle. - await step_context.context.send_activity( - MessageFactory.text( - f'Unrecognized InvokeName: "{activity.name}".', - input_hint=InputHints.ignoring_input, - ) - ) - - return DialogTurnResult(DialogTurnStatus.Complete) - - async def _on_message_activity( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - activity = step_context.context.activity - - if not self._luis_recognizer.is_configured: - await step_context.context.send_activity( - MessageFactory.text( - "NOTE: LUIS is not configured. To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and" - " 'LuisAPIHostName' to the config.py file.", - input_hint=InputHints.ignoring_input, - ) - ) - else: - # Call LUIS with the utterance. - luis_result = await self._luis_recognizer.recognize(step_context.context) - - message = f'LUIS results for "{activity.Text}":\n' - intent, intent_score = None, None - if luis_result.intents: - max_value_key = max( - luis_result.intents, key=lambda key: luis_result.intents[key] - ) - intent, intent_score = max_value_key, luis_result.intents[max_value_key] - - message += f'Intent: "{intent}" Score: {intent_score}\n' - message += f"Entities found: {len(luis_result.entities) - 1}\n" - for entity_key, entity_val in luis_result.entities: - if not entity_key == "$instance": - message += f"* {entity_val}\n" - - await step_context.context.send_activity( - MessageFactory.text(message, input_hint=InputHints.ignoring_input,) - ) - - return DialogTurnResult(DialogTurnStatus.Complete) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py deleted file mode 100644 index 538344764..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/bots/skill_bot.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ( - Bot, - ConversationState, - TurnContext, -) -from botbuilder.dialogs import Dialog, DialogExtensions - - -class SkillBot(Bot): - def __init__(self, conversation_state: ConversationState, main_dialog: Dialog): - self._conversation_state = conversation_state - self._main_dialog = main_dialog - - async def on_turn(self, context: TurnContext): - await DialogExtensions.run_dialog( - self._main_dialog, - context, - self._conversation_state.create_property("DialogState"), - ) - - # Save any state changes that might have occurred during the turn. - await self._conversation_state.save_changes(context) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py deleted file mode 100644 index 31662c36b..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/config.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 39783 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") - - CONNECTION_NAME = "" - - # If ALLOWED_CALLERS is empty, any bot can call this Skill. Add MicrosoftAppIds to restrict - # callers to only those specified. - # Example: os.environ.get("AllowedCallers", ["54d3bb6a-3b6d-4ccd-bbfd-cad5c72fb53a"]) - ALLOWED_CALLERS = os.environ.get("AllowedCallers", []) - - LUIS_APP_ID = os.environ.get("LuisAppId", "") - LUIS_API_KEY = os.environ.get("LuisAPIKey", "") - # LUIS endpoint host name, ie "westus.api.cognitive.microsoft.com" - LUIS_API_HOST_NAME = os.environ.get("LuisAPIHostName", "") diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-new-rg.json b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-new-rg.json deleted file mode 100644 index b68e51ec6..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-new-rg.json +++ /dev/null @@ -1,264 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "groupLocation": { - "type": "string", - "metadata": { - "description": "Specifies the location of the Resource Group." - } - }, - "groupName": { - "type": "string", - "metadata": { - "description": "Specifies the name of the Resource Group." - } - }, - "appId": { - "type": "string", - "metadata": { - "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." - } - }, - "appSecret": { - "type": "string", - "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." - } - }, - "botId": { - "type": "string", - "metadata": { - "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." - } - }, - "botSku": { - "type": "string", - "metadata": { - "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." - } - }, - "newAppServicePlanName": { - "type": "string", - "metadata": { - "description": "The name of the App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "newAppServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan. Defaults to \"westus\"." - } - }, - "newWebAppName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." - } - } - }, - "variables": { - "appServicePlanName": "[parameters('newAppServicePlanName')]", - "resourcesLocation": "[parameters('newAppServicePlanLocation')]", - "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", - "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", - "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]", - "publishingUsername": "[concat('$', parameters('newWebAppName'))]" - }, - "resources": [ - { - "name": "[parameters('groupName')]", - "type": "Microsoft.Resources/resourceGroups", - "apiVersion": "2018-05-01", - "location": "[parameters('groupLocation')]", - "properties": { - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2018-05-01", - "name": "storageDeployment", - "resourceGroup": "[parameters('groupName')]", - "dependsOn": [ - "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" - ], - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": {}, - "variables": {}, - "resources": [ - { - "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", - "type": "Microsoft.Web/serverfarms", - "name": "[variables('appServicePlanName')]", - "apiVersion": "2018-02-01", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "kind": "linux", - "properties": { - "name": "[variables('appServicePlanName')]", - "perSiteScaling": false, - "reserved": true, - "targetWorkerCount": 0, - "targetWorkerSizeId": 0 - } - }, - { - "comments": "Create a Web App using a Linux App Service Plan", - "type": "Microsoft.Web/sites", - "apiVersion": "2015-08-01", - "location": "[variables('resourcesLocation')]", - "kind": "app,linux", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" - ], - "name": "[variables('webAppName')]", - "properties": { - "name": "[variables('webAppName')]", - "hostNameSslStates": [ - { - "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Standard" - }, - { - "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Repository" - } - ], - "serverFarmId": "[variables('appServicePlanName')]", - "siteConfig": { - "appSettings": [ - { - "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", - "value": "true" - }, - { - "name": "MicrosoftAppId", - "value": "[parameters('appId')]" - }, - { - "name": "MicrosoftAppPassword", - "value": "[parameters('appSecret')]" - } - ], - "cors": { - "allowedOrigins": [ - "https://botservice.hosting.portal.azure.net", - "https://hosting.onecloud.azure-test.net/" - ] - } - } - } - }, - { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2016-08-01", - "name": "[concat(variables('webAppName'), '/web')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ], - "properties": { - "numberOfWorkers": 1, - "defaultDocuments": [ - "Default.htm", - "Default.html", - "Default.asp", - "index.htm", - "index.html", - "iisstart.htm", - "default.aspx", - "index.php", - "hostingstart.html" - ], - "netFrameworkVersion": "v4.0", - "phpVersion": "", - "pythonVersion": "", - "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", - "requestTracingEnabled": false, - "remoteDebuggingEnabled": false, - "remoteDebuggingVersion": "VS2017", - "httpLoggingEnabled": true, - "logsDirectorySizeLimit": 35, - "detailedErrorLoggingEnabled": false, - "publishingUsername": "[variables('publishingUsername')]", - "scmType": "None", - "use32BitWorkerProcess": true, - "webSocketsEnabled": false, - "alwaysOn": false, - "appCommandLine": "gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:APP", - "managedPipelineMode": "Integrated", - "virtualApplications": [ - { - "virtualPath": "/", - "physicalPath": "site\\wwwroot", - "preloadEnabled": false, - "virtualDirectories": null - } - ], - "winAuthAdminState": 0, - "winAuthTenantState": 0, - "customAppPoolIdentityAdminState": false, - "customAppPoolIdentityTenantState": false, - "loadBalancing": "LeastRequests", - "routingRules": [], - "experiments": { - "rampUpRules": [] - }, - "autoHealEnabled": false, - "vnetName": "", - "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", - "reservedInstanceCount": 0 - } - }, - { - "apiVersion": "2017-12-01", - "type": "Microsoft.BotService/botServices", - "name": "[parameters('botId')]", - "location": "global", - "kind": "bot", - "sku": { - "name": "[parameters('botSku')]" - }, - "properties": { - "name": "[parameters('botId')]", - "displayName": "[parameters('botId')]", - "endpoint": "[variables('botEndpoint')]", - "msaAppId": "[parameters('appId')]", - "developerAppInsightsApplicationId": null, - "developerAppInsightKey": null, - "publishingCredentials": null, - "storageResourceId": null - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" - ] - } - ], - "outputs": {} - } - } - } - ] -} \ No newline at end of file diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-preexisting-rg.json b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-preexisting-rg.json deleted file mode 100644 index 66b044b52..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/deploymentTemplates/template-with-preexisting-rg.json +++ /dev/null @@ -1,242 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appId": { - "type": "string", - "metadata": { - "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." - } - }, - "appSecret": { - "type": "string", - "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." - } - }, - "botId": { - "type": "string", - "metadata": { - "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." - } - }, - "botSku": { - "defaultValue": "F0", - "type": "string", - "metadata": { - "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." - } - }, - "newAppServicePlanName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The name of the new App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "appServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan." - } - }, - "existingAppServicePlan": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the existing App Service Plan used to create the Web App for the bot." - } - }, - "newWebAppName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." - } - } - }, - "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", - "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", - "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", - "publishingUsername": "[concat('$', parameters('newWebAppName'))]", - "resourcesLocation": "[parameters('appServicePlanLocation')]", - "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", - "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", - "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" - }, - "resources": [ - { - "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2016-09-01", - "name": "[variables('servicePlanName')]", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "kind": "linux", - "properties": { - "name": "[variables('servicePlanName')]", - "perSiteScaling": false, - "reserved": true, - "targetWorkerCount": 0, - "targetWorkerSizeId": 0 - } - }, - { - "comments": "Create a Web App using a Linux App Service Plan", - "type": "Microsoft.Web/sites", - "apiVersion": "2016-08-01", - "name": "[variables('webAppName')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" - ], - "kind": "app,linux", - "properties": { - "enabled": true, - "hostNameSslStates": [ - { - "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Standard" - }, - { - "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Repository" - } - ], - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", - "reserved": true, - "scmSiteAlsoStopped": false, - "clientAffinityEnabled": false, - "clientCertEnabled": false, - "hostNamesDisabled": false, - "containerSize": 0, - "dailyMemoryTimeQuota": 0, - "httpsOnly": false, - "siteConfig": { - "appSettings": [ - { - "name": "MicrosoftAppId", - "value": "[parameters('appId')]" - }, - { - "name": "MicrosoftAppPassword", - "value": "[parameters('appSecret')]" - }, - { - "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", - "value": "true" - } - ], - "cors": { - "allowedOrigins": [ - "https://botservice.hosting.portal.azure.net", - "https://hosting.onecloud.azure-test.net/" - ] - } - } - } - }, - { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2016-08-01", - "name": "[concat(variables('webAppName'), '/web')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ], - "properties": { - "numberOfWorkers": 1, - "defaultDocuments": [ - "Default.htm", - "Default.html", - "Default.asp", - "index.htm", - "index.html", - "iisstart.htm", - "default.aspx", - "index.php", - "hostingstart.html" - ], - "netFrameworkVersion": "v4.0", - "phpVersion": "", - "pythonVersion": "", - "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", - "requestTracingEnabled": false, - "remoteDebuggingEnabled": false, - "remoteDebuggingVersion": "VS2017", - "httpLoggingEnabled": true, - "logsDirectorySizeLimit": 35, - "detailedErrorLoggingEnabled": false, - "publishingUsername": "[variables('publishingUsername')]", - "scmType": "None", - "use32BitWorkerProcess": true, - "webSocketsEnabled": false, - "alwaysOn": false, - "appCommandLine": "gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:APP", - "managedPipelineMode": "Integrated", - "virtualApplications": [ - { - "virtualPath": "/", - "physicalPath": "site\\wwwroot", - "preloadEnabled": false, - "virtualDirectories": null - } - ], - "winAuthAdminState": 0, - "winAuthTenantState": 0, - "customAppPoolIdentityAdminState": false, - "customAppPoolIdentityTenantState": false, - "loadBalancing": "LeastRequests", - "routingRules": [], - "experiments": { - "rampUpRules": [] - }, - "autoHealEnabled": false, - "vnetName": "", - "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", - "reservedInstanceCount": 0 - } - }, - { - "apiVersion": "2017-12-01", - "type": "Microsoft.BotService/botServices", - "name": "[parameters('botId')]", - "location": "global", - "kind": "bot", - "sku": { - "name": "[parameters('botSku')]" - }, - "properties": { - "name": "[parameters('botId')]", - "displayName": "[parameters('botId')]", - "endpoint": "[variables('botEndpoint')]", - "msaAppId": "[parameters('appId')]", - "developerAppInsightsApplicationId": null, - "developerAppInsightKey": null, - "publishingCredentials": null, - "storageResourceId": null - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" - ] - } - ] -} \ No newline at end of file diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/__init__.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/__init__.py deleted file mode 100644 index b6282e719..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from .booking_details import BookingDetails -from .booking_dialog import BookingDialog -from .cancel_and_help_dialog import CancelAndHelpDialog -from .date_resolver_dialog import DateResolverDialog -from .dialog_skill_bot_recognizer import DialogSkillBotRecognizer -from .location import Location -from .oauth_test_dialog import OAuthTestDialog - - -__all__ = [ - "BookingDetails", - "BookingDialog", - "CancelAndHelpDialog", - "DateResolverDialog", - "DialogSkillBotRecognizer", - "Location", - "OAuthTestDialog", -] diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_details.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_details.py deleted file mode 100644 index 3e0a1012f..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_details.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class BookingDetails: - def __init__( - self, - destination: str = None, - origin: str = None, - travel_date: str = None, - unsupported_airports=None, - ): - if unsupported_airports is None: - unsupported_airports = [] - self.destination = destination - self.origin = origin - self.travel_date = travel_date - self.unsupported_airports = unsupported_airports - - def from_json(self, json: dict): - self.destination = json.get("destination", None) - self.origin = json.get("origin", None) - self.travel_date = json.get("travel_date", None) - self.unsupported_airports = json.get("unsupported_airports", []) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_dialog.py deleted file mode 100644 index 5b4381919..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/booking_dialog.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from datatypes_date_time.timex import Timex - -from botbuilder.dialogs import WaterfallDialog, WaterfallStepContext, DialogTurnResult -from botbuilder.dialogs.prompts import ConfirmPrompt, TextPrompt, PromptOptions -from botbuilder.core import MessageFactory -from botbuilder.schema import InputHints -from .cancel_and_help_dialog import CancelAndHelpDialog -from .date_resolver_dialog import DateResolverDialog - - -class BookingDialog(CancelAndHelpDialog): - def __init__(self, dialog_id: str = None): - super(BookingDialog, self).__init__(dialog_id or BookingDialog.__name__) - - self.add_dialog(TextPrompt(TextPrompt.__name__)) - self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__)) - self.add_dialog(DateResolverDialog(DateResolverDialog.__name__)) - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, - [ - self.destination_step, - self.origin_step, - self.travel_date_step, - self.confirm_step, - self.final_step, - ], - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def destination_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """ - If a destination city has not been provided, prompt for one. - :param step_context: - :return DialogTurnResult: - """ - booking_details = step_context.options - - if booking_details.destination is None: - message_text = "Where would you like to travel to?" - prompt_message = MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ) - return await step_context.prompt( - TextPrompt.__name__, PromptOptions(prompt=prompt_message) - ) - return await step_context.next(booking_details.destination) - - async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """ - If an origin city has not been provided, prompt for one. - :param step_context: - :return DialogTurnResult: - """ - booking_details = step_context.options - - # Capture the response to the previous step's prompt - booking_details.destination = step_context.result - if booking_details.origin is None: - message_text = "From what city will you be travelling?" - prompt_message = MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ) - return await step_context.prompt( - TextPrompt.__name__, PromptOptions(prompt=prompt_message) - ) - return await step_context.next(booking_details.origin) - - async def travel_date_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """ - If a travel date has not been provided, prompt for one. - This will use the DATE_RESOLVER_DIALOG. - :param step_context: - :return DialogTurnResult: - """ - booking_details = step_context.options - - # Capture the results of the previous step - booking_details.origin = step_context.result - if not booking_details.travel_date or self.is_ambiguous( - booking_details.travel_date - ): - return await step_context.begin_dialog( - DateResolverDialog.__name__, booking_details.travel_date - ) - return await step_context.next(booking_details.travel_date) - - async def confirm_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """ - Confirm the information the user has provided. - :param step_context: - :return DialogTurnResult: - """ - booking_details = step_context.options - - # Capture the results of the previous step - booking_details.travel_date = step_context.result - message_text = ( - f"Please confirm, I have you traveling to: { booking_details.destination } from: " - f"{ booking_details.origin } on: { booking_details.travel_date}." - ) - prompt_message = MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ) - - # Offer a YES/NO prompt. - return await step_context.prompt( - ConfirmPrompt.__name__, PromptOptions(prompt=prompt_message) - ) - - async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """ - Complete the interaction and end the dialog. - :param step_context: - :return DialogTurnResult: - """ - if step_context.result: - booking_details = step_context.options - - return await step_context.end_dialog(booking_details) - return await step_context.end_dialog() - - def is_ambiguous(self, timex: str) -> bool: - timex_property = Timex(timex) - return "definite" not in timex_property.types diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/cancel_and_help_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/cancel_and_help_dialog.py deleted file mode 100644 index f8bcc77d0..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/cancel_and_help_dialog.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import ( - ComponentDialog, - DialogContext, - DialogTurnResult, - DialogTurnStatus, -) -from botbuilder.schema import ActivityTypes, InputHints -from botbuilder.core import MessageFactory - - -class CancelAndHelpDialog(ComponentDialog): - def __init__(self, dialog_id: str): - super(CancelAndHelpDialog, self).__init__(dialog_id) - - async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: - result = await self.interrupt(inner_dc) - if result is not None: - return result - - return await super(CancelAndHelpDialog, self).on_continue_dialog(inner_dc) - - 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 = MessageFactory.text( - help_message_text, help_message_text, InputHints.expecting_input - ) - - if text in ("help", "?"): - await inner_dc.context.send_activity(help_message) - return DialogTurnResult(DialogTurnStatus.Waiting) - - cancel_message_text = "Cancelling" - cancel_message = MessageFactory.text( - cancel_message_text, cancel_message_text, InputHints.ignoring_input - ) - - if text in ("cancel", "quit"): - await inner_dc.context.send_activity(cancel_message) - return await inner_dc.cancel_all_dialogs() - - return None diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/date_resolver_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/date_resolver_dialog.py deleted file mode 100644 index c5c50463d..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/date_resolver_dialog.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from datatypes_date_time.timex import Timex - -from botbuilder.core import MessageFactory -from botbuilder.dialogs import WaterfallDialog, DialogTurnResult, WaterfallStepContext -from botbuilder.dialogs.prompts import ( - DateTimePrompt, - PromptValidatorContext, - PromptOptions, - DateTimeResolution, -) -from botbuilder.schema import InputHints -from .cancel_and_help_dialog import CancelAndHelpDialog - - -class DateResolverDialog(CancelAndHelpDialog): - def __init__(self, dialog_id: str = None): - super(DateResolverDialog, self).__init__( - dialog_id or DateResolverDialog.__name__ - ) - - self.add_dialog( - DateTimePrompt( - DateTimePrompt.__name__, DateResolverDialog.datetime_prompt_validator - ) - ) - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__ + "2", [self.initial_step, self.final_step] - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ + "2" - - async def initial_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - timex = step_context.options - - prompt_msg_text = "On what date would you like to travel?" - prompt_msg = MessageFactory.text( - prompt_msg_text, prompt_msg_text, InputHints.expecting_input - ) - - reprompt_msg_text = ( - "I'm sorry, for best results, please enter your travel date including the month, " - "day and year. " - ) - reprompt_msg = MessageFactory.text( - reprompt_msg_text, reprompt_msg_text, InputHints.expecting_input - ) - - if timex is None: - # We were not given any date at all so prompt the user. - return await step_context.prompt( - DateTimePrompt.__name__, - PromptOptions(prompt=prompt_msg, retry_prompt=reprompt_msg), - ) - # We have a Date we just need to check it is unambiguous. - if "definite" not in Timex(timex).types: - # This is essentially a "reprompt" of the data we were given up front. - return await step_context.prompt( - DateTimePrompt.__name__, PromptOptions(prompt=reprompt_msg) - ) - - return await step_context.next(DateTimeResolution(timex=timex)) - - async def final_step(self, step_context: WaterfallStepContext): - timex = step_context.result[0].timex - return await step_context.end_dialog(timex) - - @staticmethod - async def datetime_prompt_validator(prompt_context: PromptValidatorContext) -> bool: - if prompt_context.recognized.succeeded: - timex = prompt_context.recognized.value[0].timex.split("T")[0] - - # TODO: Needs TimexProperty - return "definite" in Timex(timex).types - - return False diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/dialog_skill_bot_recognizer.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/dialog_skill_bot_recognizer.py deleted file mode 100644 index 6434fb13d..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/dialog_skill_bot_recognizer.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.ai.luis import LuisApplication, LuisRecognizer -from botbuilder.core import Recognizer, RecognizerResult, TurnContext - -from config import DefaultConfig - - -class DialogSkillBotRecognizer(Recognizer): - def __init__(self, configuration: DefaultConfig): - self._recognizer = None - - luis_is_configured = ( - configuration.LUIS_APP_ID - and configuration.LUIS_API_KEY - and configuration.LUIS_API_HOST_NAME - ) - if luis_is_configured: - luis_application = LuisApplication( - configuration.LUIS_APP_ID, - configuration.LUIS_API_KEY, - "https://" + configuration.LUIS_API_HOST_NAME, - ) - - self._recognizer = LuisRecognizer(luis_application) - - @property - def is_configured(self) -> bool: - # Returns true if luis is configured in the config.py and initialized. - return self._recognizer is not None - - async def recognize(self, turn_context: TurnContext) -> RecognizerResult: - return await self._recognizer.recognize(turn_context) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/location.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/location.py deleted file mode 100644 index 3ee3406ea..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/location.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class Location: - def __init__( - self, latitude: str = None, longitude: str = None, postal_code: str = None, - ): - self.latitude = latitude - self.longitude = longitude - self.postal_code = postal_code - - def from_json(self, json: dict): - self.latitude = json.get("latitude", None) - self.longitude = json.get("longitude", None) - self.postal_code = json.get("postal_code", None) diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/oauth_test_dialog.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/oauth_test_dialog.py deleted file mode 100644 index 53321bcd8..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/dialogs/oauth_test_dialog.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import ( - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, -) -from botbuilder.dialogs.prompts import ( - ConfirmPrompt, - PromptOptions, - OAuthPrompt, - OAuthPromptSettings, -) -from botbuilder.core import BotFrameworkAdapter, MessageFactory -from botbuilder.schema import InputHints - -from config import DefaultConfig -from .cancel_and_help_dialog import CancelAndHelpDialog - - -class OAuthTestDialog(CancelAndHelpDialog): - def __init__(self, configuration: DefaultConfig): - super().__init__(OAuthTestDialog.__name__) - - self._connection_name = configuration.CONNECTION_NAME - - self.add_dialog( - OAuthPrompt( - OAuthPrompt.__name__, - OAuthPromptSettings( - connection_name=self._connection_name, - text=f"Please Sign In to connection: '{self._connection_name}'", - title="Sign In", - timeout=300000, - ), - ) - ) - - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, [self.prompt_step, self.login_step] - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def prompt_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - return await step_context.begin_dialog(OAuthPrompt.__name__) - - async def login_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - # Get the token from the previous step. - token_response = step_context.result - if token_response: - # Show the token - logged_in_message = "You are now logged in." - show_token_message = "Here is your token:" - await step_context.context.send_activity( - MessageFactory.text( - logged_in_message, logged_in_message, InputHints.ignoring_input - ) - ) - await step_context.context.send_activity( - MessageFactory.text( - f"{show_token_message} {token_response.token}", - show_token_message, - InputHints.ignoring_input, - ) - ) - - # Sign out - bot_adapter: BotFrameworkAdapter = step_context.context.adapter - await bot_adapter.sign_out_user(step_context.context, self._connection_name) - sign_out_message = "You have been signed out." - await step_context.context.send_activity( - MessageFactory.text( - sign_out_message, sign_out_message, InputHints.ignoring_input - ) - ) - - return await step_context.end_dialog() - - try_again_message = "Login was not successful please try again." - await step_context.context.send_activity( - MessageFactory.text( - try_again_message, try_again_message, InputHints.ignoring_input - ) - ) - return await step_context.end_dialog() diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/requirements.txt b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/requirements.txt deleted file mode 100644 index 353ad3a20..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -aiohttp -botbuilder-core>=4.7.1 -datatypes-date-time>=1.0.0a2 diff --git a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/skill_adapter_with_error_handler.py b/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/skill_adapter_with_error_handler.py deleted file mode 100644 index d7cf87c73..000000000 --- a/samples/experimental/skills-prototypes/dialog-to-dialog/dialog-skill-bot/skill_adapter_with_error_handler.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import sys -import traceback -from datetime import datetime - -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MessageFactory, - TurnContext, -) -from botbuilder.schema import ActivityTypes, Activity, InputHints - - -class SkillAdapterWithErrorHandler(BotFrameworkAdapter): - def __init__( - self, - settings: BotFrameworkAdapterSettings, - conversation_state: ConversationState = None, - ): - super().__init__(settings) - self._conversation_state = conversation_state - - # Catch-all for errors. - async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - traceback.print_exc() - - # Send a message to the user - error_message_text = "The skill encountered an error or bug." - error_message = MessageFactory.text( - error_message_text, error_message_text, InputHints.ignoring_input - ) - await context.send_activity(error_message) - - error_message_text = ( - "To continue to run this bot, please fix the bot source code." - ) - error_message = MessageFactory.text( - error_message_text, error_message_text, InputHints.ignoring_input - ) - await context.send_activity(error_message) - - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - # Clear out state - nonlocal self - if self._conversation_state: - try: - await self._conversation_state.delete(context) - except Exception as exception: - print( - f"\n Exception caught on attempting to Delete ConversationState : {exception}", - file=sys.stderr, - ) - traceback.print_exc() - - # Send and EndOfConversation activity to the skill caller with the error to end the conversation - # and let the caller decide what to do. - end_of_conversation = Activity(type=ActivityTypes.end_of_conversation) - end_of_conversation.code = "SkillError" - end_of_conversation.text = str(error) - await context.send_activity(end_of_conversation) - - # Send a trace activity, which will be displayed in the Bot Framework Emulator - # Note: we return the entire exception in the value property to help the developer, - # this should not be done in prod. - await context.send_trace_activity( - "OnTurnError Trace", - str(error), - "https://www.botframework.com/schemas/error", - "TurnError", - ) - - self.on_turn_error = on_error