diff --git a/tests/functional/build/templates/template-app-insights-resources.json b/build/templates/template-app-insights-resources.json similarity index 96% rename from tests/functional/build/templates/template-app-insights-resources.json rename to build/templates/template-app-insights-resources.json index 640b82e924..0fa26fa2e3 100644 --- a/tests/functional/build/templates/template-app-insights-resources.json +++ b/build/templates/template-app-insights-resources.json @@ -4,7 +4,7 @@ "parameters": { "appInsightsName": { "type": "string", - "defaultValue": "bffnappinsights", + "defaultValue": "bfcfnappinsights", "metadata": { "description": "The name of the Application Insights instance." } diff --git a/tests/functional/build/templates/template-bot-resources.json b/build/templates/template-bot-resources.json similarity index 100% rename from tests/functional/build/templates/template-bot-resources.json rename to build/templates/template-bot-resources.json diff --git a/tests/functional/build/templates/template-cosmosdb-resources.json b/build/templates/template-cosmosdb-resources.json similarity index 100% rename from tests/functional/build/templates/template-cosmosdb-resources.json rename to build/templates/template-cosmosdb-resources.json diff --git a/tests/functional/build/templates/template-key-vault-resources.json b/build/templates/template-key-vault-resources.json similarity index 100% rename from tests/functional/build/templates/template-key-vault-resources.json rename to build/templates/template-key-vault-resources.json diff --git a/tests/functional/build/templates/template-python-bot-resources.json b/build/templates/template-python-bot-resources.json similarity index 100% rename from tests/functional/build/templates/template-python-bot-resources.json rename to build/templates/template-python-bot-resources.json diff --git a/tests/functional/build/templates/template-service-plan-linux-resources.json b/build/templates/template-service-plan-linux-resources.json similarity index 100% rename from tests/functional/build/templates/template-service-plan-linux-resources.json rename to build/templates/template-service-plan-linux-resources.json diff --git a/tests/functional/build/templates/template-service-plan-windows-resources.json b/build/templates/template-service-plan-windows-resources.json similarity index 100% rename from tests/functional/build/templates/template-service-plan-windows-resources.json rename to build/templates/template-service-plan-windows-resources.json diff --git a/build/templates/template-storage-resources.json b/build/templates/template-storage-resources.json new file mode 100644 index 0000000000..5991d5cd5e --- /dev/null +++ b/build/templates/template-storage-resources.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "storageAccountsName": { + "defaultValue": "bfcfnstorage", + "type": "String" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Specifies the Azure location where the storage account should be created." + } + } + }, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-04-01", + "name": "[parameters('storageAccountsName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "kind": "StorageV2", + "properties": { + "networkAcls": { + "bypass": "AzureServices", + "virtualNetworkRules": [], + "ipRules": [], + "defaultAction": "Allow" + }, + "supportsHttpsTrafficOnly": true, + "encryption": { + "services": { + "blob": { + "keyType": "Account", + "enabled": true + } + }, + "keySource": "Microsoft.Storage" + }, + "accessTier": "Hot" + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2021-04-01", + "name": "[concat(parameters('storageAccountsName'), '/default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountsName'))]" + ], + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "cors": { + "corsRules": [] + }, + "deleteRetentionPolicy": { + "enabled": false + } + } + } + ] +} \ No newline at end of file diff --git a/tests/functional/Bots/Python/.gitignore b/tests/functional/Bots/Python/.gitignore deleted file mode 100644 index 01e8a341d5..0000000000 --- a/tests/functional/Bots/Python/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ - -# Ignore .deployment files generated to deploy Python to linux from VSCode -**/.deployment \ No newline at end of file diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/.env b/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/.env deleted file mode 100644 index 11aabb20f4..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/.env +++ /dev/null @@ -1,24 +0,0 @@ -MicrosoftAppId= -MicrosoftAppPassword= -SkillHostEndpoint=http://localhost:37000/api/skills - -skill_EchoSkillBotComposerDotNet_appId= -skill_EchoSkillBotComposerDotNet_endpoint=http://localhost:35410/api/messages - -skill_EchoSkillBotDotNet_appId= -skill_EchoSkillBotDotNet_endpoint=http://localhost:35400/api/messages - -skill_EchoSkillBotDotNet21_appId= -skill_EchoSkillBotDotNet21_endpoint=http://localhost:35405/api/messages - -skill_EchoSkillBotDotNetV3_appId= -skill_EchoSkillBotDotNetV3_endpoint=http://localhost:35407/api/messages - -skill_EchoSkillBotJS_appId= -skill_EchoSkillBotJS_endpoint=http://localhost:36400/api/messages - -skill_EchoSkillBotJSV3_appId= -skill_EchoSkillBotJSV3_endpoint=http://localhost:36407/api/messages - -skill_EchoSkillBotPython_appId= -skill_EchoSkillBotPython_endpoint=http://localhost:37400/api/messages diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/adapter_with_error_handler.py b/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/adapter_with_error_handler.py deleted file mode 100644 index f8271d257a..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/adapter_with_error_handler.py +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import sys -import traceback - -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 bots.host_bot import ACTIVE_SKILL_PROPERTY_NAME - - -class AdapterWithErrorHandler(BotFrameworkAdapter): - def __init__( - self, - settings: BotFrameworkAdapterSettings, - config: DefaultConfig, - conversation_state: ConversationState, - skill_client: SkillHttpClient = None, - skill_config: SkillConfiguration = None, - ): - super().__init__(settings) - self._config = config - - if not conversation_state: - raise TypeError( - "AdapterWithErrorHandler: `conversation_state` argument cannot be None." - ) - 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): - if not self._skill_client or not self._skill_config: - return - try: - exc_info = sys.exc_info() - stack = traceback.format_exception(*exc_info) - - # Send a message to the user. - error_message_text = "The bot encountered an error or bug." - error_message = MessageFactory.text( - error_message_text, error_message_text, InputHints.ignoring_input - ) - error_message.value = {"message": error, "stack": stack} - await turn_context.send_activity(error_message) - - await turn_context.send_activity(f"Exception: {error}") - await turn_context.send_activity(traceback.format_exc()) - - 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, which will be displayed in Bot Framework Emulator. - await turn_context.send_trace_activity( - label="TurnError", - name="on_turn_error Trace", - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - 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._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: the root bot manages the ActiveSkillPropertyName, which has a value while the root bot - # has an active conversation with a skill. - active_skill = await self._conversation_state.create_property( - ACTIVE_SKILL_PROPERTY_NAME - ).get(turn_context) - - 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): - 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" for a Web page. - 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/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/app.py b/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/app.py deleted file mode 100644 index 283492ff41..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/app.py +++ /dev/null @@ -1,89 +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, -) -from botbuilder.core.integration import ( - aiohttp_channel_service_routes, - aiohttp_error_middleware, -) -from botbuilder.core.skills import SkillHandler -from botbuilder.integration.aiohttp.skills import SkillHttpClient -from botbuilder.schema import Activity -from botframework.connector.auth import ( - AuthenticationConfiguration, - SimpleCredentialProvider, -) - -from dialogs import SetupDialog -from skill_conversation_id_factory import SkillConversationIdFactory -from authentication import AllowedSkillsClaimsValidator -from bots import HostBot -from config import DefaultConfig, SkillConfiguration -from adapter_with_error_handler import AdapterWithErrorHandler - -CONFIG = DefaultConfig() -SKILL_CONFIG = SkillConfiguration() - -# Whitelist skills from SKILL_CONFIG -AUTH_CONFIG = AuthenticationConfiguration( - claims_validator=AllowedSkillsClaimsValidator(CONFIG).claims_validator -) -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings( - app_id=CONFIG.APP_ID, - app_password=CONFIG.APP_PASSWORD, - auth_configuration=AUTH_CONFIG, -) - -STORAGE = MemoryStorage() -CONVERSATION_STATE = ConversationState(STORAGE) - -ID_FACTORY = SkillConversationIdFactory(STORAGE) -CREDENTIAL_PROVIDER = SimpleCredentialProvider(CONFIG.APP_ID, CONFIG.APP_PASSWORD) -CLIENT = SkillHttpClient(CREDENTIAL_PROVIDER, ID_FACTORY) - -ADAPTER = AdapterWithErrorHandler( - SETTINGS, CONFIG, CONVERSATION_STATE, CLIENT, SKILL_CONFIG -) - -# Create the Bot -DIALOG = SetupDialog(CONVERSATION_STATE, SKILL_CONFIG) -BOT = HostBot(CONVERSATION_STATE, SKILL_CONFIG, CLIENT, CONFIG, DIALOG) - -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/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/authentication/__init__.py b/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/authentication/__init__.py deleted file mode 100644 index 550f9b54b6..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/authentication/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .allowed_skills_claims_validator import AllowedSkillsClaimsValidator - -__all__ = ["AllowedSkillsClaimsValidator"] diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/authentication/allowed_skills_claims_validator.py b/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/authentication/allowed_skills_claims_validator.py deleted file mode 100644 index 7f0d26f087..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/authentication/allowed_skills_claims_validator.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Awaitable, Callable, Dict, List - -from botframework.connector.auth import JwtTokenValidation, SkillValidation - -from config import DefaultConfig - - -class AllowedSkillsClaimsValidator: - - config_key = "ALLOWED_CALLERS" - - def __init__(self, config: DefaultConfig): - if not config: - raise TypeError( - "AllowedSkillsClaimsValidator: 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 - caller_list = getattr(config, self.config_key) - if caller_list is None: - raise TypeError(f'"{self.config_key}" not found in configuration.') - self._allowed_callers = caller_list - - @property - def claims_validator(self) -> Callable[[List[Dict]], Awaitable]: - async def allow_callers_claims_validator(claims: Dict[str, object]): - # if allowed_callers is None we allow all calls - if "*" not in self._allowed_callers and SkillValidation.is_skill_claim( - claims - ): - # Check that the appId claim in the skill request is in the list of skills configured for this bot. - 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/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/bots/__init__.py b/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/bots/__init__.py deleted file mode 100644 index acb6f75c35..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/bots/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .host_bot import HostBot - - -__all__ = ["HostBot"] diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/bots/host_bot.py b/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/bots/host_bot.py deleted file mode 100644 index ab1e383cc8..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/bots/host_bot.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import copy -from typing import List -from botbuilder.core import ( - ActivityHandler, - ConversationState, - MessageFactory, - TurnContext, -) -from botbuilder.core.skills import BotFrameworkSkill -from botbuilder.dialogs import Dialog -from botbuilder.schema import ( - ActivityTypes, - ChannelAccount, - ExpectedReplies, - DeliveryModes, -) -from botbuilder.integration.aiohttp.skills import SkillHttpClient - -from config import DefaultConfig, SkillConfiguration -from helpers.dialog_helper import DialogHelper - -DELIVERY_MODE_PROPERTY_NAME = "deliveryModeProperty" -ACTIVE_SKILL_PROPERTY_NAME = "activeSkillProperty" - - -class HostBot(ActivityHandler): - def __init__( - self, - conversation_state: ConversationState, - skills_config: SkillConfiguration, - skill_client: SkillHttpClient, - config: DefaultConfig, - dialog: Dialog, - ): - self._bot_id = config.APP_ID - self._skill_client = skill_client - self._skills_config = skills_config - self._conversation_state = conversation_state - self._dialog = dialog - self._dialog_state_property = conversation_state.create_property("DialogState") - - # Create state property to track the delivery mode and active skill. - self._delivery_mode_property = conversation_state.create_property( - DELIVERY_MODE_PROPERTY_NAME - ) - self._active_skill_property = conversation_state.create_property( - ACTIVE_SKILL_PROPERTY_NAME - ) - - async def on_turn(self, turn_context): - # Forward all activities except EndOfConversation to the active skill. - if turn_context.activity.type != ActivityTypes.end_of_conversation: - # Try to get the active skill - active_skill: BotFrameworkSkill = await self._active_skill_property.get( - turn_context - ) - - if active_skill: - delivery_mode: str = await self._delivery_mode_property.get( - turn_context - ) - - # Send the activity to the skill - await self.__send_to_skill(turn_context, delivery_mode, active_skill) - return - - 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_message_activity(self, turn_context: TurnContext): - if turn_context.activity.text in self._skills_config.SKILLS: - delivery_mode: str = await self._delivery_mode_property.get(turn_context) - selected_skill = self._skills_config.SKILLS[turn_context.activity.text] - v3_bots = ["EchoSkillBotDotNetV3", "EchoSkillBotJSV3"] - - if ( - selected_skill - and delivery_mode == DeliveryModes.expect_replies - and selected_skill.id.lower() in (id.lower() for id in v3_bots) - ): - message = MessageFactory.text( - "V3 Bots do not support 'expectReplies' delivery mode." - ) - await turn_context.send_activity(message) - - # Forget delivery mode and skill invocation. - await self._delivery_mode_property.delete(turn_context) - - # Restart setup dialog - await self._conversation_state.delete(turn_context) - - await DialogHelper.run_dialog( - self._dialog, - turn_context, - self._dialog_state_property, - ) - - async def on_end_of_conversation_activity(self, turn_context: TurnContext): - await self.end_conversation(turn_context.activity, 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: - await turn_context.send_activity( - MessageFactory.text("Hello and welcome!") - ) - await DialogHelper.run_dialog( - self._dialog, - turn_context, - self._dialog_state_property, - ) - - async def end_conversation(self, activity, turn_context): - # Forget delivery mode and skill invocation. - await self._delivery_mode_property.delete(turn_context) - await self._active_skill_property.delete(turn_context) - - eoc_activity_message = ( - f"Received {ActivityTypes.end_of_conversation}.\n\nCode: {activity.code}." - ) - if activity.text: - eoc_activity_message = eoc_activity_message + f"\n\nText: {activity.text}" - if activity.value: - eoc_activity_message = eoc_activity_message + f"\n\nValue: {activity.value}" - await turn_context.send_activity(eoc_activity_message) - - # We are back - await turn_context.send_activity(MessageFactory.text("Back in the host bot.")) - - # Restart setup dialog. - await DialogHelper.run_dialog( - self._dialog, - turn_context, - self._dialog_state_property, - ) - - await self._conversation_state.save_changes(turn_context) - - async def __send_to_skill( - self, - turn_context: TurnContext, - delivery_mode: str, - target_skill: BotFrameworkSkill, - ): - # NOTE: Always SaveChanges() before calling a skill so that any activity generated by the skill - # will have access to current accurate state. - await self._conversation_state.save_changes(turn_context, force=True) - - if delivery_mode == "expectReplies": - # Clone activity and update its delivery mode. - activity = copy.copy(turn_context.activity) - activity.delivery_mode = delivery_mode - - # Route the activity to the skill. - expect_replies_response = await self._skill_client.post_activity_to_skill( - self._bot_id, - target_skill, - self._skills_config.SKILL_HOST_ENDPOINT, - activity, - ) - - # Route response activities back to the channel. - response_activities: ExpectedReplies = ( - ExpectedReplies().deserialize(expect_replies_response.body).activities - ) - - for response_activity in response_activities: - if response_activity.type == ActivityTypes.end_of_conversation: - await self.end_conversation(response_activity, turn_context) - - else: - await turn_context.send_activity(response_activity) - - else: - # Route the activity to the skill. - await self._skill_client.post_activity_to_skill( - self._bot_id, - target_skill, - self._skills_config.SKILL_HOST_ENDPOINT, - turn_context.activity, - ) diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/config.py b/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/config.py deleted file mode 100644 index 6e4da1d2a4..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/config.py +++ /dev/null @@ -1,71 +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 -from dotenv import load_dotenv - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - load_dotenv() - - PORT = 37000 - APP_ID = os.getenv("MicrosoftAppId") - APP_PASSWORD = os.getenv("MicrosoftAppPassword") - SKILL_HOST_ENDPOINT = os.getenv("SkillHostEndpoint") - SKILLS = [] - - # Callers to only those specified, '*' allows any caller. - # Example: os.environ.get("AllowedCallers", ["54d3bb6a-3b6d-4ccd-bbfd-cad5c72fb53a"]) - ALLOWED_CALLERS = os.environ.get("AllowedCallers", ["*"]) - - @staticmethod - def configure_skills(): - skills = list() - env_skills = [x for x in os.environ if ((x.lower().startswith("skill_")) and ('group' not in x.lower()))] - - for envKey in env_skills: - keys = envKey.split("_") - bot_id = keys[1] - key = keys[2] - index = -1 - - for i, newSkill in enumerate(skills): - if newSkill["id"] == bot_id: - index = i - - if key.lower() == "appid": - attr = "app_id" - elif key.lower() == "endpoint": - attr = "skill_endpoint" - else: - raise ValueError( - f"[SkillsConfiguration]: Invalid environment variable declaration {key}" - ) - - env_val = os.getenv(envKey) - - if index == -1: - skill = {"id": bot_id, attr: env_val} - skills.append(skill) - else: - skills[index][attr] = env_val - pass - - DefaultConfig.SKILLS = skills - - -DefaultConfig.configure_skills() - - -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/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/dialogs/__init__.py b/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/dialogs/__init__.py deleted file mode 100644 index 0ea48d3bd7..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/dialogs/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .setup_dialog import SetupDialog - -__all__ = ["SetupDialog"] diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/dialogs/setup_dialog.py b/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/dialogs/setup_dialog.py deleted file mode 100644 index fe87c91941..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/dialogs/setup_dialog.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, -) -from botbuilder.dialogs.choices.list_style import ListStyle -from botbuilder.dialogs.prompts import ( - TextPrompt, - ChoicePrompt, - PromptOptions, -) -from botbuilder.dialogs.choices import Choice -from botbuilder.core import MessageFactory, ConversationState -from botbuilder.schema import InputHints, DeliveryModes - -from bots.host_bot import ACTIVE_SKILL_PROPERTY_NAME, DELIVERY_MODE_PROPERTY_NAME -from config import SkillConfiguration - - -class SetupDialog(ComponentDialog): - def __init__( - self, conversation_state: ConversationState, skills_config: SkillConfiguration - ): - super(SetupDialog, self).__init__(SetupDialog.__name__) - - self._delivery_mode_property = conversation_state.create_property( - DELIVERY_MODE_PROPERTY_NAME - ) - self._active_skill_property = conversation_state.create_property( - ACTIVE_SKILL_PROPERTY_NAME - ) - self._delivery_mode = "" - - self._skills_config = skills_config - - # Define the setup dialog and its related components. - # Add ChoicePrompt to render available skills. - self.add_dialog(ChoicePrompt(self.select_delivery_mode_step.__name__)) - self.add_dialog(ChoicePrompt(self.select_skill_step.__name__)) - self.add_dialog(TextPrompt(self.final_step.__name__)) - # Add main waterfall dialog for this bot. - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, - [ - self.select_delivery_mode_step, - self.select_skill_step, - self.final_step, - ], - ) - ) - self.initial_dialog_id = WaterfallDialog.__name__ - - # Render a prompt to select the delivery mode to use. - async def select_delivery_mode_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - # Create the PromptOptions with the delivery modes supported. - message = "What delivery mode would you like to use?" - reprompt_message = ( - "That was not a valid choice, please select a valid delivery mode." - ) - options = PromptOptions( - prompt=MessageFactory.text(message, message, InputHints.expecting_input), - retry_prompt=MessageFactory.text( - reprompt_message, reprompt_message, InputHints.expecting_input - ), - choices=[Choice("normal"), Choice("expectReplies")], - ) - return await step_context.prompt( - self.select_delivery_mode_step.__name__, options - ) - - # Render a prompt to select the skill to call. - async def select_skill_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - # Set delivery mode. - self._delivery_mode = step_context.result.value - await self._delivery_mode_property.set( - step_context.context, step_context.result.value - ) - - # Create the PromptOptions from the skill configuration which contains the list of configured skills. - message = "What skill would you like to call?" - reprompt_message = "That was not a valid choice, please select a valid skill." - options = PromptOptions( - prompt=MessageFactory.text(message, message, InputHints.expecting_input), - retry_prompt=MessageFactory.text(reprompt_message, reprompt_message), - choices=[ - Choice(value=skill.id) - for _, skill in sorted(self._skills_config.SKILLS.items()) - ], - style=ListStyle.suggested_action - ) - - return await step_context.prompt(self.select_skill_step.__name__, options) - - # The SetupDialog has ended, we go back to the HostBot to connect with the selected skill. - async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - # Set active skill. - for i in self._skills_config.SKILLS.keys(): - if i.lower() == step_context.result.value.lower(): - selected_skill = self._skills_config.SKILLS.get(i) - - await self._active_skill_property.set(step_context.context, selected_skill) - - v3_bots = ['EchoSkillBotDotNetV3', 'EchoSkillBotJSV3'] - - if self._delivery_mode == DeliveryModes.expect_replies and selected_skill.id.lower() in (id.lower() for id in v3_bots): - message = MessageFactory.text("V3 Bots do not support 'expectReplies' delivery mode.") - await step_context.context.send_activity(message) - - # Forget delivery mode and skill invocation. - await self._delivery_mode_property.delete(step_context.context) - await self._active_skill_property.delete(step_context.context) - - # Restart setup dialog - return await step_context.replace_dialog(self.initial_dialog_id) - - await step_context.context.send_activity( - MessageFactory.text("Type anything to send to the skill.", "Type anything to send to the skill.", InputHints.expecting_input) - ) - - return await step_context.end_dialog() diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/helpers/dialog_helper.py b/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/helpers/dialog_helper.py deleted file mode 100644 index 6b2646b0ba..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/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/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/requirements.txt b/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/requirements.txt deleted file mode 100644 index 47a60840f5..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -botbuilder-integration-aiohttp>=4.13.0 -botbuilder-dialogs>=4.13.0 -python-dotenv diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/skill_conversation_id_factory.py b/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/skill_conversation_id_factory.py deleted file mode 100644 index 8b3ab86d14..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/SimpleHostBot/skill_conversation_id_factory.py +++ /dev/null @@ -1,75 +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 - - # Create the storage key based on the SkillConversationIdFactoryOptions. - conversation_reference = TurnContext.get_conversation_reference( - options.activity - ) - skill_conversation_id = ( - f"{conversation_reference.conversation.id}" - f"-{options.bot_framework_skill.id}" - f"-{conversation_reference.channel_id}" - f"-skillconvo" - ) - - # Create the SkillConversationReference instance. - skill_conversation_reference = SkillConversationReference( - conversation_reference=conversation_reference, - oauth_scope=options.from_bot_oauth_scope, - ) - - # Store the SkillConversationReference using the skill_conversation_id as a key. - skill_conversation_info = {skill_conversation_id: skill_conversation_reference} - await self._storage.write(skill_conversation_info) - - # Return the generated skill_conversation_id (that will be also used as the conversation ID to call the skill). - return skill_conversation_id - - 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") - - # Get the SkillConversationReference from storage for the given skill_conversation_id. - 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/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/.env b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/.env deleted file mode 100644 index 382c7b2790..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/.env +++ /dev/null @@ -1,53 +0,0 @@ -MicrosoftAppId= -MicrosoftAppPassword= -SkillHostEndpoint=http://localhost:37020/api/skills -SsoConnectionName= -SsoConnectionNameTeams= - -skill_EchoSkillBotDotNet_group=Echo -skill_EchoSkillBotDotNet_appId= -skill_EchoSkillBotDotNet_endpoint=http://localhost:35400/api/messages - -skill_EchoSkillBotDotNet21_group=Echo -skill_EchoSkillBotDotNet21_appId= -skill_EchoSkillBotDotNet21_endpoint=http://localhost:35405/api/messages - -skill_EchoSkillBotDotNetV3_group=Echo -skill_EchoSkillBotDotNetV3_appId= -skill_EchoSkillBotDotNetV3_endpoint=http://localhost:35407/api/messages - -skill_EchoSkillBotJS_group=Echo -skill_EchoSkillBotJS_appId= -skill_EchoSkillBotJS_endpoint=http://localhost:36400/api/messages - -skill_EchoSkillBotJSV3_group=Echo -skill_EchoSkillBotJSV3_appId= -skill_EchoSkillBotJSV3_endpoint=http://localhost:36407/api/messages - -skill_EchoSkillBotPython_group=Echo -skill_EchoSkillBotPython_appId= -skill_EchoSkillBotPython_endpoint=http://localhost:37400/api/messages - -skill_WaterfallSkillBotDotNet_group=Waterfall -skill_WaterfallSkillBotDotNet_appId= -skill_WaterfallSkillBotDotNet_endpoint=http://localhost:35420/api/messages - -skill_WaterfallSkillBotJS_group=Waterfall -skill_WaterfallSkillBotJS_appId= -skill_WaterfallSkillBotJS_endpoint=http://localhost:36420/api/messages - -skill_WaterfallSkillBotPython_group=Waterfall -skill_WaterfallSkillBotPython_appId= -skill_WaterfallSkillBotPython_endpoint=http://localhost:37420/api/messages - -skill_TeamsSkillBotDotNet_group=Teams -skill_TeamsSkillBotDotNet_appId= -skill_TeamsSkillBotDotNet_endpoint=http://localhost:35430/api/messages - -skill_TeamsSkillBotJS_group=Teams -skill_TeamsSkillBotJS_appId= -skill_TeamsSkillBotJS_endpoint=http://localhost:36430/api/messages - -skill_TeamsSkillBotPython_group=Teams -skill_TeamsSkillBotPython_appId= -skill_TeamsSkillBotPython_endpoint=http://localhost:37430/api/messages diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/.pylintrc b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/.pylintrc deleted file mode 100644 index 0eb8d1d4f6..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/.pylintrc +++ /dev/null @@ -1,590 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore= - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - bad-continuation, - duplicate-code, - redefined-outer-name, - missing-docstring, - too-many-instance-attributes, - too-few-public-methods, - redefined-builtin, - too-many-arguments, - no-self-use, - fixme, - broad-except, - bare-except, - too-many-public-methods, - cyclic-import, - too-many-locals, - too-many-function-args, - too-many-return-statements, - import-error, - no-name-in-module, - too-many-branches - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[LOGGING] - -# Format style used to check logging format string. `old` means using % -# formatting, while `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package.. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=120 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[STRING] - -# This flag controls whether the implicit-str-concat-in-sequence should -# generate a warning on implicit string concatenation in sequences defined over -# several lines. -check-str-concat-over-line-jumps=no - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement. -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/adapter_with_error_handler.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/adapter_with_error_handler.py deleted file mode 100644 index d2fc861c15..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/adapter_with_error_handler.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import sys -import traceback - -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MessageFactory, - TurnContext, -) -from botbuilder.integration.aiohttp.skills import SkillHttpClient -from botbuilder.schema import ActivityTypes, Activity, InputHints - -from skills_configuration import DefaultConfig, SkillsConfiguration -from bots.root_bot import ACTIVE_SKILL_PROPERTY_NAME - - -class AdapterWithErrorHandler(BotFrameworkAdapter): - def __init__( - self, - settings: BotFrameworkAdapterSettings, - config: DefaultConfig, - conversation_state: ConversationState, - skill_client: SkillHttpClient = None, - skill_config: SkillsConfiguration = None, - ): - super().__init__(settings) - self._config = config - - if not conversation_state: - raise TypeError( - "AdapterWithErrorHandler: `conversation_state` argument cannot be None." - ) - 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): - if not self._skill_client or not self._skill_config: - return - try: - exc_info = sys.exc_info() - stack = traceback.format_exception(*exc_info) - - # Send a message to the user. - error_message_text = "The bot encountered an error or bug." - error_message = MessageFactory.text( - error_message_text, error_message_text, InputHints.ignoring_input - ) - error_message.value = {"message": error, "stack": stack} - await turn_context.send_activity(error_message) - - await turn_context.send_activity(f"Exception: {error}") - await turn_context.send_activity(traceback.format_exc()) - - 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, which will be displayed in Bot Framework Emulator. - await turn_context.send_trace_activity( - label="TurnError", - name="on_turn_error Trace", - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - except Exception as exception: - print( - f"\n Exception caught in _send_error_message : {exception}", - file=sys.stderr, - ) - traceback.print_exc() - - async def _end_skill_conversation(self, turn_context: TurnContext): - if 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: the root bot manages the ActiveSkillPropertyName, which has a value while the root bot - # has an active conversation with a skill. - active_skill = await self._conversation_state.create_property( - ACTIVE_SKILL_PROPERTY_NAME - ).get(turn_context) - - 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): - try: - # Delete the conversationState for the current conversation to prevent the - # bot from getting stuck in an error-loop caused by being in a bad state. - # ConversationState should be thought of as similar to "cookie-state" for a Web page. - 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/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/app.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/app.py deleted file mode 100644 index 63bdfd6db7..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/app.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from http import HTTPStatus - -from aiohttp import web -from aiohttp.web import Request, Response -from aiohttp.web_response import json_response -from botbuilder.core import ( - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - UserState, -) -from botbuilder.core.integration import ( - aiohttp_channel_service_routes, - aiohttp_error_middleware, -) -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 skills_configuration import DefaultConfig, SkillsConfiguration -from adapter_with_error_handler import AdapterWithErrorHandler -from skill_conversation_id_factory import SkillConversationIdFactory -from token_exchange_skill_handler import TokenExchangeSkillHandler - -CONFIG = DefaultConfig() -SKILL_CONFIG = SkillsConfiguration() - -# 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) - -# Whitelist skills from SKILLS_CONFIG -AUTH_CONFIG = AuthenticationConfiguration( - claims_validator=AllowedSkillsClaimsValidator(SKILL_CONFIG).claims_validator -) - -# 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, CONFIG, CONVERSATION_STATE, CLIENT, SKILL_CONFIG -) - -DIALOG = MainDialog(CONVERSATION_STATE, ID_FACTORY, CLIENT, SKILL_CONFIG, CONFIG) - -# Create the Bot -BOT = RootBot(CONVERSATION_STATE, DIALOG) - -SKILL_HANDLER = TokenExchangeSkillHandler( - ADAPTER, - BOT, - CONFIG, - ID_FACTORY, - SKILL_CONFIG, - CLIENT, - 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=HTTPStatus.UNSUPPORTED_MEDIA_TYPE) - - activity = Activity().deserialize(body) - auth_header = req.headers["Authorization"] if "Authorization" in req.headers else "" - - invoke_response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - if invoke_response: - return json_response(data=invoke_response.body, status=invoke_response.status) - return Response(status=HTTPStatus.OK) - - -APP = web.Application(middlewares=[aiohttp_error_middleware]) -APP.router.add_post("/api/messages", messages) -APP.router.add_get("/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/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/authentication/__init__.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/authentication/__init__.py deleted file mode 100644 index 550f9b54b6..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/authentication/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .allowed_skills_claims_validator import AllowedSkillsClaimsValidator - -__all__ = ["AllowedSkillsClaimsValidator"] diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/authentication/allowed_skills_claims_validator.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/authentication/allowed_skills_claims_validator.py deleted file mode 100644 index 69bfddabc8..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/authentication/allowed_skills_claims_validator.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import Awaitable, Callable, Dict, List -from botframework.connector.auth import JwtTokenValidation, SkillValidation -from skills_configuration import SkillsConfiguration - - -class AllowedSkillsClaimsValidator: - def __init__(self, skills_config: SkillsConfiguration): - if not skills_config: - raise TypeError( - "AllowedSkillsClaimsValidator: config object cannot be None." - ) - - skills_list = [skill.app_id for skill in skills_config.SKILLS.values()] - self._allowed_skills = frozenset(skills_list) - - @property - def claims_validator(self) -> Callable[[List[Dict]], Awaitable]: - async def allow_callers_claims_validator(claims: Dict[str, object]): - if 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_skills: - 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/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/bots/__init__.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/bots/__init__.py deleted file mode 100644 index 6a5060f62f..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/bots/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .root_bot import RootBot - - -__all__ = ["RootBot"] diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/bots/root_bot.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/bots/root_bot.py deleted file mode 100644 index 3ad5213c8a..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/bots/root_bot.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -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, ChannelAccount, Attachment - -DELIVERY_MODE_PROPERTY_NAME = "deliveryModeProperty" -ACTIVE_SKILL_PROPERTY_NAME = "activeSkillProperty" - - -class RootBot(ActivityHandler): - def __init__( - self, - conversation_state: ConversationState, - main_dialog: Dialog, - ): - self._conversation_state = conversation_state - self._main_dialog = main_dialog - - self._dialog_state_property = conversation_state.create_property("DialogState") - - # Create state property to track the delivery mode and active skill. - self._delivery_mode_property = conversation_state.create_property( - DELIVERY_MODE_PROPERTY_NAME - ) - self._active_skill_property = conversation_state.create_property( - ACTIVE_SKILL_PROPERTY_NAME - ) - - async def on_turn(self, turn_context: TurnContext): - if turn_context.activity.type != ActivityTypes.conversation_update: - # Run the Dialog with the Activity. - await DialogExtensions.run_dialog( - self._main_dialog, - turn_context, - self._dialog_state_property, - ) - else: - # Let the base class handle the activity. - 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 - ): - # Greet anyone that was not the target (recipient) of this message. - # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards. - 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) - activity.speak = "Welcome to the waterfall host bot" - await turn_context.send_activity(activity) - await DialogExtensions.run_dialog( - self._main_dialog, - turn_context, - self._dialog_state_property, - ) - - @staticmethod - def _create_adaptive_card_attachment() -> Attachment: - """ - Load attachment from embedded resource. - """ - - relative_path = os.path.abspath(os.path.dirname(__file__)) - path = os.path.join(relative_path, "../cards/welcomeCard.json") - with open(path) as in_file: - card = json.load(in_file) - - return Attachment( - content_type="application/vnd.microsoft.card.adaptive", content=card - ) diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/cards/welcomeCard.json b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/cards/welcomeCard.json deleted file mode 100644 index 0cb25e2570..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/cards/welcomeCard.json +++ /dev/null @@ -1,29 +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": "Medium", - "weight": "Bolder", - "text": "Welcome to the Skill Dialog Sample!", - "wrap": true, - "maxLines": 0, - "color": "Accent" - }, - { - "type": "TextBlock", - "size": "default", - "text": "This sample allows you to connect to a skill using a SkillDialog and invoke several actions.", - "wrap": true, - "maxLines": 0 - } - ] -} \ No newline at end of file diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/__init__.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/__init__.py deleted file mode 100644 index 3f3a5f6f9a..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .main_dialog import MainDialog -from .tangent_dialog import TangentDialog -from .sso import SsoDialog, SsoSignInDialog - -__all__ = ["MainDialog", "TangentDialog", "SsoDialog", "SsoSignInDialog"] diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/main_dialog.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/main_dialog.py deleted file mode 100644 index 46981efa20..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/main_dialog.py +++ /dev/null @@ -1,456 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import copy -import json - -from botbuilder.dialogs import ( - ComponentDialog, - DialogContext, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, - ListStyle, -) -from botbuilder.dialogs.choices import Choice, FoundChoice -from botbuilder.dialogs.prompts import ( - PromptOptions, - ChoicePrompt, -) -from botbuilder.dialogs.skills import ( - SkillDialogOptions, - SkillDialog, - BeginSkillDialogOptions, -) -from botbuilder.core import ConversationState, MessageFactory, TurnContext -from botbuilder.core.skills import ConversationIdFactoryBase -from botbuilder.schema import Activity, ActivityTypes, InputHints, DeliveryModes -from botbuilder.integration.aiohttp.skills import SkillHttpClient - -from skills_configuration import SkillsConfiguration, DefaultConfig - -from dialogs.tangent_dialog import TangentDialog -from dialogs.sso.sso_dialog import SsoDialog - -SSO_DIALOG_PREFIX = "Sso" -ACTIVE_SKILL_PROPERTY_NAME = "MainDialog.ActiveSkillProperty" -DELIVERY_MODE_NAME = "MainDialog.DeliveryMode" -SELECTED_SKILL_KEY_NAME = "MainDialog.SelectedSkillKey" -JUST_FORWARD_THE_ACTIVITY = "JustForwardTurnContext.Activity" - -DELIVERY_MODE_PROMPT = "DeliveryModePrompt" -SKILL_GROUP_PROMPT = "SkillGroupPrompt" -SKILL_PROMPT = "SkillPrompt" -SKILL_ACTION_PROMPT = "SkillActionPrompt" - - -class MainDialog(ComponentDialog): - def __init__( - self, - conversation_state: ConversationState, - conversation_id_factory: ConversationIdFactoryBase, - skill_client: SkillHttpClient, - skills_config: SkillsConfiguration, - configuration: DefaultConfig, - ): - super().__init__(MainDialog.__name__) - - self._configuration = configuration - if not self._configuration: - raise TypeError( - "[MainDialog]: Missing parameter. configuration is required" - ) - - bot_id = self._configuration.APP_ID - - self._skills_config = skills_config - if not self._skills_config: - raise TypeError( - "[MainDialog]: Missing parameter. skills_config is required" - ) - - if not skill_client: - raise TypeError("[MainDialog]: Missing parameter. skill_client is required") - - if not conversation_state: - raise TypeError( - "[MainDialog]: Missing parameter. conversation_state is required" - ) - - if not conversation_id_factory: - raise TypeError( - "[MainDialog]: Missing parameter. conversation_id_factory is required" - ) - - # Use helper method to add SkillDialog instances for the configured skills. - self._add_skill_dialogs( - conversation_state, - conversation_id_factory, - skill_client, - skills_config, - bot_id, - ) - - # Create state property to track the active skill. - self.active_skill_property = conversation_state.create_property( - ACTIVE_SKILL_PROPERTY_NAME - ) - - # Register the tangent dialog for testing tangents and resume. - self.add_dialog(TangentDialog()) - - # Add ChoicePrompt to render available delivery modes. - self.add_dialog(ChoicePrompt(DELIVERY_MODE_PROMPT)) - - # Add ChoicePrompt to render available types of skill. - self.add_dialog(ChoicePrompt(SKILL_GROUP_PROMPT)) - - # Add ChoicePrompt to render available skills. - self.add_dialog(ChoicePrompt(SKILL_PROMPT)) - - # Add ChoicePrompt to render skill actions. - self.add_dialog(ChoicePrompt(SKILL_ACTION_PROMPT)) - - # Special case: register SSO dialogs for skills that support SSO actions. - self._add_sso_dialogs(self._configuration) - - # Add main waterfall dialog for this bot. - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, - [ - self._select_delivery_mode_step, - self._select_skill_group_step, - self._select_skill_step, - self._select_skill_action_step, - self._call_skill_action_step, - self._final_step, - ], - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def on_continue_dialog(self, inner_dc: DialogContext): - """ - This override is used to test the "abort" command to interrupt skills from the parent and - also to test the "tangent" command to start a tangent and resume a skill. - """ - - # 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 activity.text - and "abort" in activity.text.lower() - ): - # Cancel all dialogs when the user says abort. - # The SkillDialog automatically sends an EndOfConversation message to the skill to let the - # skill know that it needs to end its current dialogs, too. - await inner_dc.cancel_all_dialogs() - return await inner_dc.replace_dialog( - self.initial_dialog_id, - "Canceled! \n\n What delivery mode would you like to use?", - ) - - # Sample to test a tangent when in the middle of a skill conversation. - if ( - active_skill - and activity.type == ActivityTypes.message - and activity.text - and activity.text.lower() == "tangent" - ): - # Start tangent. - return await inner_dc.begin_dialog(TangentDialog.__name__) - - return await super().on_continue_dialog(inner_dc) - - async def _select_delivery_mode_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """ - Render a prompt to select the delivery mode to use. - """ - - # Create the PromptOptions with the delivery modes supported. - message_text = ( - str(step_context.options) - if step_context.options - else "What delivery mode would you like to use?" - ) - retry_message_text = ( - "That was not a valid choice, please select a valid delivery mode." - ) - - options = PromptOptions( - prompt=MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ), - retry_prompt=MessageFactory.text( - retry_message_text, retry_message_text, InputHints.expecting_input - ), - choices=[ - Choice(DeliveryModes.normal.value), - Choice(DeliveryModes.expect_replies.value), - ], - ) - - # Prompt the user to select a delivery mode. - return await step_context.prompt(DELIVERY_MODE_PROMPT, options) - - async def _select_skill_group_step(self, step_context: WaterfallStepContext): - """ - Render a prompt to select the group of skills to use. - """ - - # Remember the delivery mode selected by the user. - step_context.values[DELIVERY_MODE_NAME] = step_context.result.value - - # Create the PromptOptions with the types of supported skills. - message_text = "What group of skills would you like to use?" - retry_message_text = ( - "That was not a valid choice, please select a valid skill group." - ) - - choices = [] - groups = [] - for skill in self._skills_config.SKILLS.values(): - if skill.group not in groups: - groups.append(skill.group) - choices.append(Choice(skill.group)) - - options = PromptOptions( - prompt=MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ), - retry_prompt=MessageFactory.text( - retry_message_text, retry_message_text, InputHints.expecting_input - ), - choices=choices, - ) - - # Prompt the user to select a type of skill. - return await step_context.prompt(SKILL_GROUP_PROMPT, options) - - async def _select_skill_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """ - Render a prompt to select the skill to call. - """ - - skill_group = step_context.result.value - - # Create the PromptOptions from the skill configuration which contain the list of configured skills. - message_text = "What skill would you like to call?" - retry_message_text = "That was not a valid choice, please select a valid skill." - - options = PromptOptions( - prompt=MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ), - retry_prompt=MessageFactory.text( - retry_message_text, retry_message_text, InputHints.expecting_input - ), - style=ListStyle.list_style, - choices=[ - Choice(skill.id) - for skill in self._skills_config.SKILLS.values() - if skill.group.lower().startswith(skill_group.lower()) - ], - ) - - # Prompt the user to select a skill. - return await step_context.prompt(SKILL_PROMPT, options) - - async def _select_skill_action_step(self, step_context: WaterfallStepContext): - """ - Render a prompt to select the begin action for the skill. - """ - - # Get the skill info based on the selected skill. - selected_skill_id = step_context.result.value - delivery_mode = str(step_context.values[DELIVERY_MODE_NAME]) - v3_bots = ["EchoSkillBotDotNetV3", "EchoSkillBotJSV3"] - - # Exclude v3 bots from ExpectReplies - if ( - delivery_mode == DeliveryModes.expect_replies - and selected_skill_id in v3_bots - ): - await step_context.context.send_activity( - MessageFactory.text( - "V3 Bots do not support 'expectReplies' delivery mode." - ) - ) - - # Restart setup dialog - return await step_context.replace_dialog(self.initial_dialog_id) - - selected_skill = next( - val - for key, val in self._skills_config.SKILLS.items() - if val.id == selected_skill_id - ) - - # Remember the skill selected by the user. - step_context.values[SELECTED_SKILL_KEY_NAME] = selected_skill - - skill_action_choices = [ - Choice(action) for action in selected_skill.get_actions() - ] - if len(skill_action_choices) == 1: - # The skill only supports one action (e.g. Echo), skip the prompt. - return await step_context.next( - FoundChoice(value=skill_action_choices[0].value, index=0, score=0) - ) - - # Create the PromptOptions with the actions supported by the selected skill. - message_text = f"Select an action to send to **{selected_skill.id}**." - - options = PromptOptions( - prompt=MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ), - choices=skill_action_choices, - ) - - # Prompt the user to select a skill action. - return await step_context.prompt(SKILL_ACTION_PROMPT, options) - - async def _call_skill_action_step(self, step_context: WaterfallStepContext): - """ - Starts the SkillDialog based on the user's selections. - """ - - selected_skill = step_context.values[SELECTED_SKILL_KEY_NAME] - - # Save active skill in state. - await self.active_skill_property.set(step_context.context, selected_skill) - - # Create the initial activity to call the skill. - skill_activity = self._create_begin_activity( - step_context.context, selected_skill.id, step_context.result.value - ) - - if skill_activity.name == "Sso": - # Special case, we start the SSO dialog to prepare the host to call the skill. - return await step_context.begin_dialog( - f"{SSO_DIALOG_PREFIX}{selected_skill.id}" - ) - - # 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_activity.channel_data = step_context.context.activity.channel_data - skill_activity.additional_properties = ( - step_context.context.activity.additional_properties - ) - - # Create the BeginSkillDialogOptions and assign the activity to send. - skill_dialog_args = BeginSkillDialogOptions(activity=skill_activity) - if str(step_context.values[DELIVERY_MODE_NAME]) == DeliveryModes.expect_replies: - skill_dialog_args.activity.delivery_mode = DeliveryModes.expect_replies - - # Start the skillDialog instance with the arguments. - return await step_context.begin_dialog(selected_skill.id, skill_dialog_args) - - async def _final_step(self, step_context: WaterfallStepContext): - """ - The SkillDialog has ended, render the results (if any) and restart MainDialog. - """ - - active_skill = await self.active_skill_property.get(step_context.context) - - # Check if the skill returned any results and display them. - if step_context.result: - message = f'Skill "{active_skill.id}" invocation complete.' - message += f" Result: {json.dumps(step_context.result)}" - await step_context.context.send_activity( - MessageFactory.text(message, message, InputHints.ignoring_input) - ) - - # Clear the delivery mode selected by the user. - step_context.values[DELIVERY_MODE_NAME] = None - - # Clear the skill selected by the user. - step_context.values[SELECTED_SKILL_KEY_NAME] = 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, - f'Done with "{active_skill.id}". \n\n What delivery mode would you ' - f"like to use?", - ) - - def _add_skill_dialogs( - self, - conversation_state: ConversationState, - conversation_id_factory: ConversationIdFactoryBase, - skill_client: SkillHttpClient, - skills_config: SkillsConfiguration, - bot_id: str, - ): - """ - Helper method that creates and adds SkillDialog instances for the configured skills. - """ - - for skill_info in self._skills_config.SKILLS.values(): - # Create the dialog options. - 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, - conversation_state=conversation_state, - skill=skill_info, - ) - - # Add a SkillDialog for the selected skill. - self.add_dialog(SkillDialog(skill_dialog_options, skill_info.id)) - - def _create_begin_activity( - self, context: TurnContext, skill_id: str, selected_option: str - ): - if selected_option.lower() == JUST_FORWARD_THE_ACTIVITY.lower(): - # Note message activities also support input parameters but we are not using them in this example. - # Return a deep clone of the activity so we don't risk altering the original one - return copy.deepcopy(context.activity) - - # Get the begin activity from the skill instance. - activity: Activity = self._skills_config.SKILLS[skill_id].create_begin_activity( - selected_option - ) - - # 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. - activity.channel_data = context.activity.channel_data - activity.additional_properties = context.activity.additional_properties - - return activity - - # Special case. - # SSO needs a dialog in the host to allow the user to sign in. - # We create and several SsoDialog instances for each skill that supports SSO. - def _add_sso_dialogs(self, configuration: DefaultConfig): - connection_name = configuration.SSO_CONNECTION_NAME - - for sso_skill_dialog in [ - skill_dialog - for skill_dialog in self._dialogs._dialogs.values() # pylint: disable=W0212 - if skill_dialog.id.startswith("WATERFALLSKILL") - ]: - self.add_dialog( - SsoDialog( - f"{SSO_DIALOG_PREFIX}{sso_skill_dialog.id}", - sso_skill_dialog, - connection_name, - ) - ) diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/sso/__init__.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/sso/__init__.py deleted file mode 100644 index f489fd4fae..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/sso/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .sso_dialog import SsoDialog -from .sso_signin_dialog import SsoSignInDialog - -__all__ = ["SsoDialog", "SsoSignInDialog"] diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/sso/sso_dialog.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/sso/sso_dialog.py deleted file mode 100644 index 1145358a33..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/sso/sso_dialog.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, - Choice, - ChoicePrompt, - PromptOptions, - Dialog, - BeginSkillDialogOptions, -) -from botbuilder.core import MessageFactory -from botbuilder.dialogs.dialog_turn_result import DialogTurnResult -from botbuilder.dialogs.dialog_turn_status import DialogTurnStatus -from botbuilder.schema import Activity, ActivityTypes, InputHints - -from .sso_signin_dialog import SsoSignInDialog - - -class SsoDialog(ComponentDialog): - """ - Helps prepare the host for SSO operations and provides helpers to check the status and invoke the skill. - """ - - def __init__(self, dialog_id: str, sso_skill_dialog: Dialog, connection_name): - super().__init__(dialog_id) - - self._connection_name = connection_name - self.skill_dialog_id = sso_skill_dialog.id - - self.add_dialog(ChoicePrompt(ChoicePrompt.__name__)) - self.add_dialog(SsoSignInDialog(self._connection_name)) - self.add_dialog(sso_skill_dialog) - - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, - [ - self.prompt_action_step, - self.handle_action_step, - self.prompt_final_step, - ], - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def prompt_action_step(self, step_context: WaterfallStepContext): - message_text = "What SSO action do you want to perform?" - reprompt_message_text = ( - "That was not a valid choice, please select a valid choice." - ) - - options = PromptOptions( - prompt=MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ), - retry_prompt=MessageFactory.text( - reprompt_message_text, reprompt_message_text, InputHints.expecting_input - ), - choices=await self.get_prompt_choices(step_context), - ) - - # Prompt the user to select an SSO action. - return await step_context.prompt(ChoicePrompt.__name__, options) - - async def get_prompt_choices(self, step_context: WaterfallStepContext): - """ - Create the prompt choices based on the current sign in status - """ - - prompt_choices = list() - token = await step_context.context.adapter.get_user_token( - step_context.context, self._connection_name - ) - - if token is None: - prompt_choices.append(Choice("Login")) - - # Token exchange will fail when the host is not logged on and the skill should - # show a regular OAuthPrompt - prompt_choices.append(Choice("Call Skill (without SSO)")) - else: - prompt_choices.append(Choice("Logout")) - prompt_choices.append(Choice("Show token")) - prompt_choices.append(Choice("Call Skill (with SSO)")) - - prompt_choices.append(Choice("Back")) - - return prompt_choices - - async def handle_action_step(self, step_context: WaterfallStepContext): - action = str(step_context.result.value).lower() - - if action == "login": - return await step_context.begin_dialog(SsoSignInDialog.__name__) - - if action == "logout": - await step_context.context.adapter.sign_out_user( - step_context.context, self._connection_name - ) - await step_context.context.send_activity("You have been signed out.") - return await step_context.next(step_context.result) - - if action == "show token": - token = await step_context.context.adapter.get_user_token( - step_context.context, self._connection_name - ) - - if token is None: - await step_context.context.send_activity("User has no cached token.") - else: - await step_context.context.send_activity( - f"Here is your current SSO token: { token.token }" - ) - - return await step_context.next(step_context.result) - - if action in ["call skill (with sso)", "call skill (without sso)"]: - begin_skill_activity = Activity(type=ActivityTypes.event, name="Sso") - - return await step_context.begin_dialog( - self.skill_dialog_id, - BeginSkillDialogOptions(activity=begin_skill_activity), - ) - - if action == "back": - return DialogTurnResult(DialogTurnStatus.Complete) - - # This should never be hit since the previous prompt validates the choice - raise Exception(f"Unrecognized action: {action}") - - async def prompt_final_step(self, step_context: WaterfallStepContext): - # Restart the dialog (we will exit when the user says end) - return await step_context.replace_dialog(self.initial_dialog_id) diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/sso/sso_signin_dialog.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/sso/sso_signin_dialog.py deleted file mode 100644 index 242679dc36..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/sso/sso_signin_dialog.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, - OAuthPrompt, - OAuthPromptSettings, -) - - -class SsoSignInDialog(ComponentDialog): - def __init__(self, connection_name: str): - super().__init__(SsoSignInDialog.__name__) - - self.add_dialog( - OAuthPrompt( - OAuthPrompt.__name__, - OAuthPromptSettings( - connection_name=connection_name, - text=f"Sign in to the host bot using AAD for SSO and connection {connection_name}", - title="Sign In", - timeout=60000, - ), - ) - ) - - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, - [ - self.signin_step, - self.display_token, - ], - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def signin_step(self, step_context: WaterfallStepContext): - return await step_context.begin_dialog(OAuthPrompt.__name__) - - async def display_token(self, step_context: WaterfallStepContext): - sso_token = step_context.result - if sso_token: - if isinstance(sso_token, dict): - token = sso_token.get("token") - else: - token = sso_token.token - - await step_context.context.send_activity(f"Here is your token: {token}") - - else: - await step_context.context.send_activity("No token was provided.") - - return await step_context.end_dialog() diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/tangent_dialog.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/tangent_dialog.py deleted file mode 100644 index 957aba021b..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/dialogs/tangent_dialog.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, - TextPrompt, - PromptOptions, -) -from botbuilder.core import MessageFactory -from botbuilder.schema import InputHints - - -class TangentDialog(ComponentDialog): - """ - A simple waterfall dialog used to test triggering tangents from "MainDialog". - """ - - def __init__(self): - super().__init__(TangentDialog.__name__) - - self.add_dialog(TextPrompt(TextPrompt.__name__)) - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, [self._step_1, self._step_2, self._end_step] - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def _step_1(self, step_context: WaterfallStepContext): - message_text = "Tangent step 1 of 2, say something." - prompt_message = MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ) - return await step_context.prompt( - TextPrompt.__name__, PromptOptions(prompt=prompt_message) - ) - - async def _step_2(self, step_context: WaterfallStepContext): - message_text = "Tangent step 2 of 2, say something." - prompt_message = MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ) - return await step_context.prompt( - TextPrompt.__name__, PromptOptions(prompt=prompt_message) - ) - - async def _end_step(self, step_context: WaterfallStepContext): - return await step_context.end_dialog() diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/helpers/dialog_helper.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/helpers/dialog_helper.py deleted file mode 100644 index 6b2646b0ba..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/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/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/requirements.txt b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/requirements.txt deleted file mode 100644 index 02460b14f9..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -botbuilder-integration-aiohttp>=4.13.0 -botbuilder-dialogs>=4.13.0 -python-dotenv~=0.15.0 diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skill_conversation_id_factory.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skill_conversation_id_factory.py deleted file mode 100644 index 06c5e5a2f5..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skill_conversation_id_factory.py +++ /dev/null @@ -1,76 +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 - - # Create the storage key based on the SkillConversationIdFactoryOptions. - conversation_reference = TurnContext.get_conversation_reference( - options.activity - ) - skill_conversation_id = ( - f"{conversation_reference.conversation.id}" - f"-{options.bot_framework_skill.id}" - f"-{conversation_reference.channel_id}" - f"-skillconvo" - ) - - # Create the SkillConversationReference instance. - skill_conversation_reference = SkillConversationReference( - conversation_reference=conversation_reference, - oauth_scope=options.from_bot_oauth_scope, - ) - - # Store the SkillConversationReference using the skill_conversation_id as a key. - skill_conversation_info = {skill_conversation_id: skill_conversation_reference} - await self._storage.write(skill_conversation_info) - - # Return the generated skill_conversation_id (that will be also used as the conversation ID to call the skill). - return skill_conversation_id - - 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") - - # Get the SkillConversationReference from storage for the given skill_conversation_id. - 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/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills/echo_skill.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills/echo_skill.py deleted file mode 100644 index 560f0adb14..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills/echo_skill.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from enum import Enum -from botbuilder.schema import Activity -from skills.skill_definition import SkillDefinition - - -class EchoSkill(SkillDefinition): - class SkillAction(str, Enum): - MESSAGE = "Message" - - def get_actions(self): - return self.SkillAction - - def create_begin_activity(self, action_id: str): - if action_id not in self.SkillAction: - raise Exception(f'Unable to create begin activity for "${action_id}".') - - # We only support one activity for Echo so no further checks are needed - activity = Activity.create_message_activity() - activity.name = self.SkillAction.MESSAGE.value - activity.text = "Begin the Echo Skill" - - return activity diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills/skill_definition.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills/skill_definition.py deleted file mode 100644 index 3e0d43e498..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills/skill_definition.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core.skills import BotFrameworkSkill - - -class SkillDefinition(BotFrameworkSkill): - """ - Extends BotFrameworkSkill and provides methods to return the actions and the begin activity - to start a skill. - This class also exposes a group property to render skill groups and narrow down the available - options. - Remarks: This is just a temporary implementation, ideally, this should be replaced by logic that - parses a manifest and creates what's needed. - """ - - def __init__(self, id: str = None, group: str = None): - super().__init__(id=id) - self.group = group - - def get_actions(self): - raise NotImplementedError("[SkillDefinition]: Method not implemented") - - def create_begin_activity(self, action_id: str): - raise NotImplementedError("[SkillDefinition]: Method not implemented") diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills/teams_skill.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills/teams_skill.py deleted file mode 100644 index 6631c869ea..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills/teams_skill.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from enum import Enum -from botbuilder.schema import Activity -from skills.skill_definition import SkillDefinition - - -class TeamsSkill(SkillDefinition): - class SkillAction(str, Enum): - TEAMS_TASK_MODULE = "TeamsTaskModule" - TEAMS_CARD_ACTION = "TeamsCardAction" - TEAMS_CONVERSATION = "TeamsConversation" - CARDS = "Cards" - PROACTIVE = "Proactive" - ATTACHMENT = "Attachment" - AUTH = "Auth" - SSO = "Sso" - ECHO = "Echo" - FILE_UPLOAD = "FileUpload" - DELETE = "Delete" - UPDATE = "Update" - - def get_actions(self): - return self.SkillAction - - def create_begin_activity(self, action_id: str): - if action_id not in self.SkillAction: - raise Exception(f'Unable to create begin activity for "${action_id}".') - - # We don't support special parameters in these skills so a generic event with the - # right name will do in this case. - activity = Activity.create_event_activity() - activity.name = action_id - - return activity diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills/waterfall_skill.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills/waterfall_skill.py deleted file mode 100644 index 34db3b6abc..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills/waterfall_skill.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from enum import Enum -from botbuilder.schema import Activity -from skills.skill_definition import SkillDefinition - - -class WaterfallSkill(SkillDefinition): - class SkillAction(str, Enum): - CARDS = "Cards" - PROACTIVE = "Proactive" - AUTH = "Auth" - MESSAGE_WITH_ATTACHMENT = "MessageWithAttachment" - SSO = "Sso" - FILE_UPLOAD = "FileUpload" - ECHO = "Echo" - DELETE = "Delete" - UPDATE = "Update" - - def get_actions(self): - return self.SkillAction - - def create_begin_activity(self, action_id: str): - if action_id not in self.SkillAction: - raise Exception(f'Unable to create begin activity for "${action_id}".') - - # We don't support special parameters in these skills so a generic event with the - # right name will do in this case. - activity = Activity.create_event_activity() - activity.name = action_id - - return activity diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills_configuration.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills_configuration.py deleted file mode 100644 index 1b29374ffb..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/skills_configuration.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -from typing import Dict - -from botbuilder.dialogs import ObjectPath -from dotenv import load_dotenv - -from skills.skill_definition import SkillDefinition -from skills.waterfall_skill import WaterfallSkill -from skills.echo_skill import EchoSkill -from skills.teams_skill import TeamsSkill - -load_dotenv() - - -class DefaultConfig: - """ - Bot Default Configuration - """ - - PORT = 37020 - APP_ID = os.getenv("MicrosoftAppId") - APP_PASSWORD = os.getenv("MicrosoftAppPassword") - SSO_CONNECTION_NAME = os.getenv("SsoConnectionName") - SSO_CONNECTION_NAME_TEAMS = os.getenv("SsoConnectionNameTeams") - - -class SkillsConfiguration: - """ - Bot Skills Configuration - A helper class that loads Skills information from configuration - Remarks: This class loads the skill settings from env and casts them into derived - types of SkillDefinition so we can render prompts with the skills and in their - groups. - """ - - SKILL_HOST_ENDPOINT = os.getenv("SkillHostEndpoint") - SKILLS: Dict[str, SkillDefinition] = dict() - - def __init__(self): - skills_data = dict() - skill_variable = [x for x in os.environ if x.lower().startswith("skill_")] - - for val in skill_variable: - names = val.split("_") - bot_id = names[1] - attr = names[2] - - if bot_id not in skills_data: - skills_data[bot_id] = dict() - - if attr.lower() == "appid": - skills_data[bot_id]["app_id"] = os.getenv(val) - elif attr.lower() == "endpoint": - skills_data[bot_id]["skill_endpoint"] = os.getenv(val) - elif attr.lower() == "group": - skills_data[bot_id]["group"] = os.getenv(val) - else: - raise ValueError( - f"[SkillsConfiguration]: Invalid environment variable declaration {attr}" - ) - - for skill_id, skill_value in skills_data.items(): - definition = SkillDefinition(id=skill_id, group=skill_value["group"]) - definition.app_id = skill_value["app_id"] - definition.skill_endpoint = skill_value["skill_endpoint"] - self.SKILLS[skill_id] = self.create_skill_definition(definition) - - # Note: we hard code this for now, we should dynamically create instances based on the manifests. - # For now, this code creates a strong typed version of the SkillDefinition based on the skill group - # and copies the info from env into it. - @staticmethod - def create_skill_definition(skill: SkillDefinition): - if skill.group.lower() == ("echo"): - skill_definition = ObjectPath.assign(EchoSkill(), skill) - - elif skill.group.lower() == ("waterfall"): - skill_definition = ObjectPath.assign(WaterfallSkill(), skill) - - elif skill.group.lower() == ("teams"): - skill_definition = ObjectPath.assign(TeamsSkill(), skill) - - else: - raise Exception(f"Unable to find definition class for {skill.id}.") - - return skill_definition diff --git a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/token_exchange_skill_handler.py b/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/token_exchange_skill_handler.py deleted file mode 100644 index 1da847ef5f..0000000000 --- a/tests/functional/Bots/Python/Consumers/CodeFirst/WaterfallHostBot/token_exchange_skill_handler.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import traceback -from uuid import uuid4 -from typing import Union - -from botbuilder.core import Bot, TurnContext -from botbuilder.core.card_factory import ContentTypes -from botbuilder.core.skills import ( - SkillHandler, - BotFrameworkSkill, -) -from botbuilder.integration.aiohttp.skills import SkillHttpClient -from botbuilder.schema import ( - ResourceResponse, - ActivityTypes, - SignInConstants, - TokenExchangeInvokeRequest, -) -from botframework.connector.auth import ( - CredentialProvider, - AuthenticationConfiguration, - ClaimsIdentity, - Activity, - JwtTokenValidation, -) -from botframework.connector.token_api.models import TokenExchangeRequest - -from adapter_with_error_handler import AdapterWithErrorHandler -from skill_conversation_id_factory import SkillConversationIdFactory -from skills.skill_definition import SkillDefinition -from skills_configuration import SkillsConfiguration, DefaultConfig - - -class TokenExchangeSkillHandler(SkillHandler): - def __init__( - self, - adapter: AdapterWithErrorHandler, - bot: Bot, - configuration: DefaultConfig, - conversation_id_factory: SkillConversationIdFactory, - skills_config: SkillsConfiguration, - skill_client: SkillHttpClient, - credential_provider: CredentialProvider, - auth_configuration: AuthenticationConfiguration, - ): - super().__init__( - adapter, - bot, - conversation_id_factory, - credential_provider, - auth_configuration, - ) - self._token_exchange_provider = adapter - if not self._token_exchange_provider: - raise ValueError( - f"{self._token_exchange_provider} does not support token exchange" - ) - - self._configuration = configuration - self._skills_config = skills_config - self._skill_client = skill_client - self._conversation_id_factory = conversation_id_factory - self._bot_id = configuration.APP_ID - - async def on_send_to_conversation( - self, claims_identity: ClaimsIdentity, conversation_id: str, activity: Activity, - ) -> ResourceResponse: - if await self._intercept_oauth_cards(claims_identity, activity): - return ResourceResponse(id=str(uuid4())) - - return await super().on_send_to_conversation( - claims_identity, conversation_id, activity - ) - - async def on_reply_to_activity( - self, - claims_identity: ClaimsIdentity, - conversation_id: str, - activity_id: str, - activity: Activity, - ) -> ResourceResponse: - if await self._intercept_oauth_cards(claims_identity, activity): - return ResourceResponse(id=str(uuid4())) - - return await super().on_reply_to_activity( - claims_identity, conversation_id, activity_id, activity - ) - - def _get_calling_skill( - self, claims_identity: ClaimsIdentity - ) -> Union[SkillDefinition, None]: - app_id = JwtTokenValidation.get_app_id_from_claims(claims_identity.claims) - - if not app_id: - return None - - return next( - skill - for skill in self._skills_config.SKILLS.values() - if skill.app_id == app_id - ) - - async def _intercept_oauth_cards( - self, claims_identity: ClaimsIdentity, activity: Activity - ) -> bool: - if activity.attachments: - oauth_card_attachment = next( - ( - attachment - for attachment in activity.attachments - if attachment.content_type == ContentTypes.oauth_card - ), - None, - ) - - if oauth_card_attachment: - target_skill = self._get_calling_skill(claims_identity) - if target_skill: - oauth_card = oauth_card_attachment.content - token_exchange_resource = oauth_card.get( - "TokenExchangeResource" - ) or oauth_card.get("tokenExchangeResource") - if token_exchange_resource: - context = TurnContext(self._adapter, activity) - context.turn_state["BotIdentity"] = claims_identity - - # We need to know what connection name to use for the token exchange so we figure that out here - connection_name = ( - self._configuration.SSO_CONNECTION_NAME - if target_skill.group == "Waterfall" - else self._configuration.SSO_CONNECTION_NAME_TEAMS - ) - - if not connection_name: - raise ValueError("The SSO connection name cannot be null.") - - # AAD token exchange - try: - uri = token_exchange_resource.get("uri") - result = await self._token_exchange_provider.exchange_token( - context, - connection_name, - activity.recipient.id, - TokenExchangeRequest(uri=uri), - ) - - if result.token: - # If token above is null, then SSO has failed and hence we return false. - # If not, send an invoke to the skill with the token. - return await self._send_token_exchange_invoke_to_skill( - incoming_activity=activity, - connection_name=oauth_card.get("connectionName"), - resource_id=token_exchange_resource.get("id"), - token=result.token, - target_skill=target_skill, - ) - - except Exception as exception: - print(f"Unable to exchange token: {exception}") - traceback.print_exc() - return False - - return False - - async def _send_token_exchange_invoke_to_skill( - self, - incoming_activity: Activity, - resource_id: str, - token: str, - connection_name: str, - target_skill: BotFrameworkSkill, - ) -> bool: - activity = incoming_activity.create_reply() - activity.type = ActivityTypes.invoke - activity.name = SignInConstants.token_exchange_operation_name - activity.value = TokenExchangeInvokeRequest( - id=resource_id, token=token, connection_name=connection_name - ) - skill_conversation_reference = await self._conversation_id_factory.get_conversation_reference( - incoming_activity.conversation.id - ) - activity.conversation = ( - skill_conversation_reference.conversation_reference.conversation - ) - activity.service_url = ( - skill_conversation_reference.conversation_reference.service_url - ) - - # Route the activity to the skill - response = await self._skill_client.post_activity_to_skill( - from_bot_id=self._bot_id, - to_skill=target_skill, - service_url=self._skills_config.SKILL_HOST_ENDPOINT, - activity=activity, - ) - - return 200 <= response.status <= 299 diff --git a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/app.py b/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/app.py deleted file mode 100644 index 5a87c33513..0000000000 --- a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/app.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json -import sys -import traceback -from datetime import datetime - -from aiohttp import web -from aiohttp.web import Request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - TurnContext, - MessageFactory, -) -from botbuilder.schema import Activity, ActivityTypes, InputHints -from botframework.connector.auth import AuthenticationConfiguration - -from bots import EchoBot -from config import DefaultConfig -from authentication import AllowedCallersClaimsValidator -from http import HTTPStatus - -CONFIG = DefaultConfig() -CLAIMS_VALIDATOR = AllowedCallersClaimsValidator(frozenset(CONFIG.ALLOWED_CALLERS)) -AUTH_CONFIG = AuthenticationConfiguration( - claims_validator=CLAIMS_VALIDATOR.validate_claims -) -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings( - app_id=CONFIG.APP_ID, - app_password=CONFIG.APP_PASSWORD, - auth_configuration=AUTH_CONFIG, -) -ADAPTER = BotFrameworkAdapter(SETTINGS) - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # 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() - - try: - exc_info = sys.exc_info() - stack = traceback.format_exception(*exc_info) - - # Send a message to the user - error_message_text = "The skill encountered an error or bug." - error_message = MessageFactory.text( - f"{error_message_text}\r\n{error}\r\n{stack}", - error_message_text, - InputHints.ignoring_input, - ) - error_message.value = {"message": error, "stack": stack} - 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.expecting_input - ) - await context.send_activity(error_message) - - # Send a trace activity, which will be displayed in 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", - ) - await context.send_activity(trace_activity) - - # Send and EndOfConversation activity to the skill caller with the error to end the conversation and let the - # caller decide what to do. Send a trace activity if we're talking to the Bot Framework Emulator - end_of_conversation = Activity( - type=ActivityTypes.end_of_conversation, code="SkillError", text=f"{error}" - ) - await context.send_activity(end_of_conversation) - except Exception as exception: - print( - f"\n Exception caught on on_error : {exception}", file=sys.stderr, - ) - traceback.print_exc() - - -ADAPTER.on_turn_error = on_error - -# Create 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=HTTPStatus.UNSUPPORTED_MEDIA_TYPE) - - activity = Activity().deserialize(body) - auth_header = req.headers["Authorization"] if "Authorization" in req.headers else "" - - try: - response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - # DeliveryMode => Expected Replies - if response: - body = json.dumps(response.body) - return Response(status=response.status, body=body) - - # DeliveryMode => Normal - return Response(status=HTTPStatus.CREATED) - except Exception as exception: - raise exception - - -APP = web.Application() -APP.router.add_post("/api/messages", messages) - -# simple way of exposing the manifest for dev purposes. -APP.router.add_static("/manifests", "./manifests/") - - -if __name__ == "__main__": - try: - web.run_app(APP, host="localhost", port=CONFIG.PORT) - except Exception as error: - raise error diff --git a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/authentication/__init__.py b/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/authentication/__init__.py deleted file mode 100644 index b6383973c8..0000000000 --- a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/authentication/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .allowed_callers_claims_validator import AllowedCallersClaimsValidator - -__all__ = ["AllowedCallersClaimsValidator"] diff --git a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/authentication/allowed_callers_claims_validator.py b/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/authentication/allowed_callers_claims_validator.py deleted file mode 100644 index d790e36f34..0000000000 --- a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/authentication/allowed_callers_claims_validator.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botframework.connector.auth import JwtTokenValidation, SkillValidation - - -class AllowedCallersClaimsValidator(): - """ - Sample claims validator that loads an allowed list from configuration if present and checks - that requests are coming from allowed parent bots. - """ - - def __init__(self, allowed_callers: frozenset): - self.allowed_callers = allowed_callers - - # Load the AppIds for the configured callers (we will only allow responses from parent bots we have configured). - # DefaultConfig.ALLOWED_CALLERS is the list of parent bot Ids that are allowed to access the skill. - # To add a new parent bot simply go to the config.py file and add - # the parent bot's Microsoft AppId to the array under AllowedCallers, e.g.: - # AllowedCallers=["195bd793-4319-4a84-a800-386770c058b2","38c74e7a-3d01-4295-8e66-43dd358920f8"] - async def validate_claims(self, claims: dict): - if SkillValidation.is_skill_claim(claims) and self.allowed_callers: - # 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 ValueError( - f'Received a request from an application with an appID of "{ app_id }". To enable requests' - ' from this bot, add the id to your configuration file.' - ) diff --git a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/bots/__init__.py b/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/bots/__init__.py deleted file mode 100644 index f95fbbbadd..0000000000 --- a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/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/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/bots/echo_bot.py b/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/bots/echo_bot.py deleted file mode 100644 index 4c69deb366..0000000000 --- a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/bots/echo_bot.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ActivityHandler, MessageFactory, TurnContext -from botbuilder.schema import Activity, ActivityTypes, EndOfConversationCodes - -class EchoBot(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - if "end" in turn_context.activity.text or "stop" in turn_context.activity.text: - # Send End of conversation at the end. - await turn_context.send_activity( - MessageFactory.text("Ending conversation from the skill...") - ) - - end_of_conversation = Activity(type=ActivityTypes.end_of_conversation) - end_of_conversation.code = EndOfConversationCodes.completed_successfully - await turn_context.send_activity(end_of_conversation) - else: - await turn_context.send_activity( - MessageFactory.text(f"Echo: {turn_context.activity.text}") - ) - await turn_context.send_activity( - MessageFactory.text( - f'Say "end" or "stop" and I\'ll end the conversation and back to the parent.' - ) - ) - - async def on_end_of_conversation_activity(self, turn_context: TurnContext): - # This will be called if the host bot is ending the conversation. Sending additional messages should be - # avoided as the conversation may have been deleted. - # Perform cleanup of resources if needed. - pass diff --git a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/config.py b/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/config.py deleted file mode 100644 index 8de1e0a9b4..0000000000 --- a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/config.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/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 = 37400 - 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", "3851a47b-53ed-4d29-b878-6e941da61e98"]) - ALLOWED_CALLERS = os.environ.get("AllowedCallers", []) diff --git a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/manifests/echoskillbot-manifest-1.0.json b/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/manifests/echoskillbot-manifest-1.0.json deleted file mode 100644 index 9e995c0101..0000000000 --- a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/manifests/echoskillbot-manifest-1.0.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://schemas.botframework.com/schemas/skills/v2.1/skill-manifest.json", - "$id": "EchoSkillBotPython", - "name": "EchoSkillBotPython", - "version": "1.0", - "description": "This is a skill for echoing what the user sent to the bot (using Python).", - "publisherName": "Microsoft", - "privacyUrl": "https://microsoft.com/privacy", - "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.", - "license": "https://github.com/microsoft/BotFramework-FunctionalTests/blob/main/LICENSE", - "tags": [ - "echo" - ], - "endpoints": [ - { - "name": "default", - "protocol": "BotFrameworkV3", - "description": "Localhost endpoint for the skill (on port 37400)", - "endpointUrl": "http://localhost:37400/api/messages", - "msAppId": "00000000-0000-0000-0000-000000000000" - } - ] -} diff --git a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/requirements.txt b/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/requirements.txt deleted file mode 100644 index 3658c09bb6..0000000000 --- a/tests/functional/Bots/Python/Skills/CodeFirst/EchoSkillBot/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -botbuilder-integration-aiohttp>=4.13.0 diff --git a/tests/functional/Bots/Python/Skills/CodeFirst/WaterfallSkillBot/.env b/tests/functional/Bots/Python/Skills/CodeFirst/WaterfallSkillBot/.env deleted file mode 100644 index e83f3f1d74..0000000000 --- a/tests/functional/Bots/Python/Skills/CodeFirst/WaterfallSkillBot/.env +++ /dev/null @@ -1,11 +0,0 @@ -MicrosoftAppId= -MicrosoftAppPassword= -ConnectionName=TestOAuthProvider -SsoConnectionName= -ChannelService= -AllowedCallers=* -SkillHostEndpoint=http://localhost:37420/api/skills - -EchoSkillInfo_id=EchoSkillBot -EchoSkillInfo_appId= -EchoSkillInfo_skillEndpoint=http://localhost:37400/api/messages diff --git a/tests/functional/Bots/Python/Skills/CodeFirst/WaterfallSkillBot/.pylintrc b/tests/functional/Bots/Python/Skills/CodeFirst/WaterfallSkillBot/.pylintrc deleted file mode 100644 index 6e8d5976b6..0000000000 --- a/tests/functional/Bots/Python/Skills/CodeFirst/WaterfallSkillBot/.pylintrc +++ /dev/null @@ -1,582 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - bad-continuation, - duplicate-code, - redefined-outer-name, - missing-docstring, - too-many-instance-attributes, - too-few-public-methods, - redefined-builtin, - too-many-arguments, - no-self-use, - fixme, - broad-except, - bare-except, - too-many-public-methods, - cyclic-import, - too-many-locals, - too-many-function-args, - too-many-return-statements, - import-error, - no-name-in-module, - too-many-branches - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[LOGGING] - -# Format style used to check logging format string. `old` means using % -# formatting, while `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package.. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=120 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[STRING] - -# This flag controls whether the implicit-str-concat-in-sequence should -# generate a warning on implicit string concatenation in sequences defined over -# several lines. -check-str-concat-over-line-jumps=no - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement. -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception diff --git a/tests/functional/Bots/Python/Skills/CodeFirst/WaterfallSkillBot/app.py b/tests/functional/Bots/Python/Skills/CodeFirst/WaterfallSkillBot/app.py deleted file mode 100644 index 354fe400a8..0000000000 --- a/tests/functional/Bots/Python/Skills/CodeFirst/WaterfallSkillBot/app.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -from datetime import datetime -from http import HTTPStatus -from typing import Dict -from aiohttp import web -from aiohttp.web import Request, Response, json_response -from botbuilder.core import ( - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - TurnContext, -) -from botbuilder.core.skills import SkillHandler -from botbuilder.core.integration import ( - aiohttp_channel_service_routes, - aiohttp_error_middleware, -) -from botbuilder.integration.aiohttp.skills import SkillHttpClient -from botbuilder.schema import Activity -from botframework.connector.auth import ( - AuthenticationConfiguration, - SimpleCredentialProvider, -) -from authentication import AllowedCallersClaimsValidator -from bots import SkillBot -from config import DefaultConfig -from dialogs import ActivityRouterDialog -from dialogs.proactive import ContinuationParameters -from middleware import SsoSaveStateMiddleware -from skill_conversation_id_factory import SkillConversationIdFactory -from skill_adapter_with_error_handler import AdapterWithErrorHandler - -CONFIG = DefaultConfig() - -# Create MemoryStorage and ConversationState. -MEMORY = MemoryStorage() -CONVERSATION_STATE = ConversationState(MEMORY) - -# Create the conversationIdFactory. -CONVERSATION_ID_FACTORY = SkillConversationIdFactory(MEMORY) - -# Create the credential provider. -CREDENTIAL_PROVIDER = SimpleCredentialProvider(CONFIG.APP_ID, CONFIG.APP_PASSWORD) - -VALIDATOR = AllowedCallersClaimsValidator(CONFIG).claims_validator -AUTH_CONFIG = AuthenticationConfiguration(claims_validator=VALIDATOR) - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings( - app_id=CONFIG.APP_ID, - app_password=CONFIG.APP_PASSWORD, - auth_configuration=AUTH_CONFIG, -) -ADAPTER = AdapterWithErrorHandler(SETTINGS, CONVERSATION_STATE) - -ADAPTER.use(SsoSaveStateMiddleware(CONVERSATION_STATE)) - -# Create the skill client. -SKILL_CLIENT = SkillHttpClient(CREDENTIAL_PROVIDER, CONVERSATION_ID_FACTORY) - -CONTINUATION_PARAMETERS_STORE: Dict[str, ContinuationParameters] = dict() - -# Create the main dialog. -DIALOG = ActivityRouterDialog( - configuration=CONFIG, - conversation_state=CONVERSATION_STATE, - conversation_id_factory=CONVERSATION_ID_FACTORY, - skill_client=SKILL_CLIENT, - continuation_parameters_store=CONTINUATION_PARAMETERS_STORE, -) - -# Create the bot that will handle incoming messages. -BOT = SkillBot(CONFIG, CONVERSATION_STATE, DIALOG) -SKILL_HANDLER = SkillHandler( - ADAPTER, BOT, CONVERSATION_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=HTTPStatus.UNSUPPORTED_MEDIA_TYPE) - - activity = Activity().deserialize(body) - auth_header = req.headers["Authorization"] if "Authorization" in req.headers else "" - - try: - website_hostname = os.getenv("WEBSITE_HOSTNAME") - if website_hostname: - CONFIG.SERVER_URL = f"https://{website_hostname}" - else: - CONFIG.SERVER_URL = f"{req.scheme}://{req.host}" - - response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - # DeliveryMode => Expected Replies - if response: - return json_response(data=response.body, status=response.status) - - # DeliveryMode => Normal - return Response(status=HTTPStatus.CREATED) - except Exception as exception: - raise exception - - -# Listen for incoming requests on /api/notify -async def notify(req: Request) -> Response: - error = "" - user = req.query.get("user") - - continuation_parameters = CONTINUATION_PARAMETERS_STORE.get(user) - - if not continuation_parameters: - return Response( - content_type="text/html", - status=HTTPStatus.OK, - body=f"