diff --git a/samples/11.qnamaker/README.md b/samples/11.qnamaker/README.md new file mode 100644 index 000000000..27edff425 --- /dev/null +++ b/samples/11.qnamaker/README.md @@ -0,0 +1,56 @@ +# QnA Maker + +Bot Framework v4 QnA Maker bot sample + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a bot that uses the [QnA Maker Cognitive AI](https://www.qnamaker.ai) service. + +The [QnA Maker Service](https://www.qnamaker.ai) enables you to build, train and publish a simple question and answer bot based on FAQ URLs, structured documents or editorial content in minutes. In this sample, we demonstrate how to use the QnA Maker service to answer questions based on a FAQ text file used as input. + +## Prerequisites + +This samples **requires** prerequisites in order to run. + +### Overview + +This bot uses [QnA Maker Service](https://www.qnamaker.ai), an AI based cognitive service, to implement simple Question and Answer conversational patterns. + +### Create a QnAMaker Application to enable QnA Knowledge Bases + +QnA knowledge base setup and application configuration steps can be found [here](https://aka.ms/qna-instructions). + +## Running the sample +- Clone the repository +```bash +git clone https://github.com/Microsoft/botbuilder-python.git +``` +- Activate your desired virtual environment +- Bring up a terminal, navigate to `botbuilder-python\samples\11.qnamaker` folder +- In the terminal, type `pip install -r requirements.txt` +- Update `QNA_KNOWLEDGEBASE_ID`, `QNA_ENDPOINT_KEY`, and `QNA_ENDPOINT_HOST` in `config.py` +- In the terminal, type `python app.py` + +## Testing the bot using Bot Framework Emulator +[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + +- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + +### Connect to bot using Bot Framework Emulator +- Launch Bot Framework Emulator +- File -> Open Bot +- Paste this URL in the emulator window - http://localhost:3978/api/messages + +## QnA Maker service + +QnA Maker enables you to power a question and answer service from your semi-structured content. + +One of the basic requirements in writing your own bot is to seed it with questions and answers. In many cases, the questions and answers already exist in content like FAQ URLs/documents, product manuals, etc. With QnA Maker, users can query your application in a natural, conversational manner. QnA Maker uses machine learning to extract relevant question-answer pairs from your content. It also uses powerful matching and ranking algorithms to provide the best possible match between the user query and the questions. + +## Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [QnA Maker Documentation](https://docs.microsoft.com/en-us/azure/cognitive-services/qnamaker/overview/overview) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [QnA Maker CLI](https://github.com/Microsoft/botbuilder-tools/tree/master/packages/QnAMaker) +- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) +- [Azure Portal](https://portal.azure.com) diff --git a/samples/11.qnamaker/app.py b/samples/11.qnamaker/app.py new file mode 100644 index 000000000..1f8c6f97f --- /dev/null +++ b/samples/11.qnamaker/app.py @@ -0,0 +1,82 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import asyncio +import sys +from datetime import datetime + +from flask import Flask, request, Response +from botbuilder.core import BotFrameworkAdapterSettings, TurnContext, BotFrameworkAdapter +from botbuilder.schema import Activity, ActivityTypes + +from bots import QnABot + +# Create the loop and Flask app +LOOP = asyncio.get_event_loop() +app = Flask(__name__, instance_relative_config=True) +app.config.from_object("config.DefaultConfig") + +# Create adapter. +# See https://aka.ms/about-bot-adapter to learn more about how bots work. +SETTINGS = BotFrameworkAdapterSettings(app.config["APP_ID"], app.config["APP_PASSWORD"]) +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) + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") + await context.send_activity("To continue to run this bot, please fix the bot source code.") + # Send a trace activity if we're talking to the Bot Framework Emulator + if context.activity.channel_id == 'emulator': + # Create a trace activity that contains the error object + trace_activity = Activity( + label="TurnError", + name="on_turn_error Trace", + timestamp=datetime.utcnow(), + type=ActivityTypes.trace, + value=f"{error}", + value_type="https://www.botframework.com/schemas/error" + ) + # Send a trace activity, which will be displayed in Bot Framework Emulator + await context.send_activity(trace_activity) + +ADAPTER.on_turn_error = on_error + +# Create the Bot +BOT = QnABot(app.config) + +# Listen for incoming requests on /api/messages +@app.route("/api/messages", methods=["POST"]) +def messages(): + # Main bot message handler. + if "application/json" in request.headers["Content-Type"]: + body = request.json + else: + return Response(status=415) + + activity = Activity().deserialize(body) + auth_header = ( + request.headers["Authorization"] if "Authorization" in request.headers else "" + ) + + try: + task = LOOP.create_task( + ADAPTER.process_activity(activity, auth_header, BOT.on_turn) + ) + LOOP.run_until_complete(task) + return Response(status=201) + except Exception as exception: + raise exception + + +if __name__ == "__main__": + try: + app.run(debug=False, port=app.config["PORT"]) # nosec debug + except Exception as exception: + raise exception diff --git a/samples/11.qnamaker/bots/__init__.py b/samples/11.qnamaker/bots/__init__.py new file mode 100644 index 000000000..457940100 --- /dev/null +++ b/samples/11.qnamaker/bots/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .qna_bot import QnABot + +__all__ = ["QnABot"] diff --git a/samples/11.qnamaker/bots/qna_bot.py b/samples/11.qnamaker/bots/qna_bot.py new file mode 100644 index 000000000..8ff589e07 --- /dev/null +++ b/samples/11.qnamaker/bots/qna_bot.py @@ -0,0 +1,37 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from flask import Config + +from botbuilder.ai.qna import QnAMaker, QnAMakerEndpoint +from botbuilder.core import ActivityHandler, MessageFactory, TurnContext +from botbuilder.schema import ChannelAccount + + +class QnABot(ActivityHandler): + def __init__(self, config: Config): + self.qna_maker = QnAMaker( + QnAMakerEndpoint( + knowledge_base_id=config["QNA_KNOWLEDGEBASE_ID"], + endpoint_key=config["QNA_ENDPOINT_KEY"], + host=config["QNA_ENDPOINT_HOST"], + ) + ) + + async def on_members_added_activity( + self, members_added: [ChannelAccount], turn_context: TurnContext + ): + for member in members_added: + if member.id != turn_context.activity.recipient.id: + await turn_context.send_activity( + "Welcome to the QnA Maker sample! Ask me a question and I will try " + "to answer it." + ) + + async def on_message_activity(self, turn_context: TurnContext): + # The actual call to the QnA Maker service. + response = await self.qna_maker.get_answers(turn_context) + if response and len(response) > 0: + await turn_context.send_activity(MessageFactory.text(response[0].answer)) + else: + await turn_context.send_activity("No QnA Maker answers were found.") diff --git a/samples/11.qnamaker/cognitiveModels/smartLightFAQ.tsv b/samples/11.qnamaker/cognitiveModels/smartLightFAQ.tsv new file mode 100644 index 000000000..754118909 --- /dev/null +++ b/samples/11.qnamaker/cognitiveModels/smartLightFAQ.tsv @@ -0,0 +1,15 @@ +Question Answer Source Keywords +Question Answer 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Source +My Contoso smart light won't turn on. Check the connection to the wall outlet to make sure it's plugged in properly. 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial +Light won't turn on. Check the connection to the wall outlet to make sure it's plugged in properly. 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial +My smart light app stopped responding. Restart the app. If the problem persists, contact support. 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial +How do I contact support? Email us at service@contoso.com 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial +I need help. Email us at service@contoso.com 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial +I upgraded the app and it doesn't work anymore. When you upgrade, you need to disable Bluetooth, then re-enable it. After re-enable, re-pair your light with the app. 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial +Light doesn't work after upgrade. When you upgrade, you need to disable Bluetooth, then re-enable it. After re-enable, re-pair your light with the app. 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial +Question Answer 890b1efc-27ad-4dca-8dcb-b20d29d50e14-KB.tsv Source +Who should I contact for customer service? Please direct all customer service questions to (202) 555-0164 \n 890b1efc-27ad-4dca-8dcb-b20d29d50e14-KB.tsv Editorial +Why does the light not work? The simplest way to troubleshoot your smart light is to turn it off and on. \n 890b1efc-27ad-4dca-8dcb-b20d29d50e14-KB.tsv Editorial +How long does the light's battery last for? The battery will last approximately 10 - 12 weeks with regular use. \n 890b1efc-27ad-4dca-8dcb-b20d29d50e14-KB.tsv Editorial +What type of light bulb do I need? A 26-Watt compact fluorescent light bulb that features both energy savings and long-life performance. 890b1efc-27ad-4dca-8dcb-b20d29d50e14-KB.tsv Editorial +Hi Hello Editorial \ No newline at end of file diff --git a/samples/11.qnamaker/config.py b/samples/11.qnamaker/config.py new file mode 100644 index 000000000..068a30d35 --- /dev/null +++ b/samples/11.qnamaker/config.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + +""" Bot Configuration """ + + +class DefaultConfig: + """ Bot Configuration """ + + PORT = 3978 + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") + QNA_KNOWLEDGEBASE_ID = os.environ.get("QnAKnowledgebaseId", "") + QNA_ENDPOINT_KEY = os.environ.get("QnAEndpointKey", "") + QNA_ENDPOINT_HOST = os.environ.get("QnAEndpointHostName", "") diff --git a/samples/11.qnamaker/deploymentTemplates/template-with-preexisting-rg.json b/samples/11.qnamaker/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..bff8c096d --- /dev/null +++ b/samples/11.qnamaker/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,242 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "F0", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2016-09-01", + "name": "[variables('servicePlanName')]", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "name": "[variables('servicePlanName')]", + "perSiteScaling": false, + "reserved": true, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2016-08-01", + "name": "[variables('webAppName')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" + ], + "kind": "app,linux", + "properties": { + "enabled": true, + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "reserved": true, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": false, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "siteConfig": { + "appSettings": [ + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + }, + { + "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", + "value": "true" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2016-08-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "phpVersion": "", + "pythonVersion": "", + "nodeVersion": "", + "linuxFxVersion": "PYTHON|3.7", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "remoteDebuggingVersion": "VS2017", + "httpLoggingEnabled": true, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": false, + "appCommandLine": "", + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": false, + "virtualDirectories": null + } + ], + "winAuthAdminState": 0, + "winAuthTenantState": 0, + "customAppPoolIdentityAdminState": false, + "customAppPoolIdentityTenantState": false, + "loadBalancing": "LeastRequests", + "routingRules": [], + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "vnetName": "", + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} \ No newline at end of file diff --git a/samples/11.qnamaker/requirements.txt b/samples/11.qnamaker/requirements.txt new file mode 100644 index 000000000..cf76fec34 --- /dev/null +++ b/samples/11.qnamaker/requirements.txt @@ -0,0 +1,3 @@ +botbuilder-core>=4.4.0b1 +botbuilder-ai>=4.4.0b1 +flask>=1.0.3