diff --git a/.DS_Store b/.DS_Store index ae0f623..cbd669d 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd50f0a --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# basic_bot + +echo_template + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back. + +## Prerequisites + +This sample **requires** prerequisites in order to run. + +### Install Python 3.6 + +## Running the sample +- Run `pip install -r requirements.txt` to install all dependencies +- Run `python app.py` + + +## Testing the bot using Bot Framework Emulator + +[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 version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + +### Connect to the bot using Bot Framework Emulator + +- Launch Bot Framework Emulator +- Enter a Bot URL of `http://localhost:3978/api/messages` + + +## 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) +- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) +- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) +- [Azure Portal](https://portal.azure.com) +- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/) +- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/__pycache__/bot.cpython-37.pyc b/__pycache__/bot.cpython-37.pyc new file mode 100644 index 0000000..8ac509f Binary files /dev/null and b/__pycache__/bot.cpython-37.pyc differ diff --git a/__pycache__/config.cpython-37.pyc b/__pycache__/config.cpython-37.pyc new file mode 100644 index 0000000..9ddb732 Binary files /dev/null and b/__pycache__/config.cpython-37.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..91d4cb1 --- /dev/null +++ b/app.py @@ -0,0 +1,94 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import sys +import traceback +from datetime import datetime + +from aiohttp import web +from aiohttp.web import Request, Response, json_response +from botbuilder.core import ( + BotFrameworkAdapter, + BotFrameworkAdapterSettings, + ConversationState, + MemoryStorage, + TurnContext, + UserState, +) +from botbuilder.core.integration import aiohttp_error_middleware +from botbuilder.schema import Activity, ActivityTypes + +from bot import MyBot +from config import DefaultConfig + +CONFIG = DefaultConfig() + +# 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 = 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() + + # 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 + +# where the memory goes +memory = MemoryStorage() +user_state = UserState(memory) +conversation_state = ConversationState(memory) + +# Create the Bot +BOT = MyBot(user_state, conversation_state) + + +# Listen for incoming requests on /api/messages +async def messages(req: Request) -> Response: + # Main bot message handler. + if "application/json" in req.headers["Content-Type"]: + body = await req.json() + else: + return Response(status=415) + + activity = Activity().deserialize(body) + auth_header = req.headers["Authorization"] if "Authorization" in req.headers else "" + + response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn) + if response: + return json_response(data=response.body, status=response.status) + return Response(status=201) + + +APP = web.Application(middlewares=[aiohttp_error_middleware]) +APP.router.add_post("/api/messages", messages) + +if __name__ == "__main__": + try: + web.run_app(APP, host="localhost", port=CONFIG.PORT) + except Exception as error: + raise error diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..1475b27 --- /dev/null +++ b/bot.py @@ -0,0 +1,295 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# async lib +import asyncio +import aiohttp + +# standard python lib +import os +import json + +# bot builder lib +from botbuilder.core import ActivityHandler, TurnContext, CardFactory, MessageFactory, ConversationState, UserState +from botbuilder.schema import ChannelAccount, HeroCard, CardImage, CardAction, ActionTypes, ActivityTypes + +# state-related lib +from state_management import ConversationData, UserProfile + +class MyBot(ActivityHandler): + def __init__(self, user_state: UserState, conversation_state: ConversationState): + self.conversation_state = conversation_state + self.user_state = user_state + + self.conversation_state_accessor = self.conversation_state.create_property("ConversationData") + self.user_state_accessor = self.user_state.create_property("UserProfile") + + self.user_profile = None + self.conversation_data = None + + # API endpoint + self.API_base = 'https://ujiyan-web-app.azurewebsites.net/' + + # See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. + + async def on_turn(self, turn_context: TurnContext): + await super().on_turn(turn_context) + + await self.conversation_state.save_changes(turn_context) + await self.user_state.save_changes(turn_context) + + async def reset_and_submit(self): + # aiohttp session + session = aiohttp.ClientSession() + + # sumbit + submit_url = os.path.join(self.API_base,'submissions','create') + + # answer dict creation + answers = [] + _keys = sorted(self.conversation_data.answers.keys()) + for _key in _keys: + answers.append({'answer':self.conversation_data.answers[_key]['ans'], + 'problem_id':self.conversation_data.answers[_key]['q_id']}) + + params = {'student_id':self.user_profile.student_ID, 'test_id':self.conversation_data.test_ID, 'submissions': answers} + # print(submit_url) + # print(params) + resp = await session.post(submit_url, json=params) + # print(resp.status) + + await session.close() + + # reset conversation state + self.conversation_data.problem_set = [] + self.conversation_data.counter = 1 + self.conversation_data.answers = {} + self.conversation_data.test_ID = '' + self.conversation_data.test_title = '' + self.conversation_data.on_test_session = False + self.conversation_data.on_submit_session = False + + async def asign_test_ID(self, test_id): + self.conversation_data.test_ID = test_id + + async def switch_on_test_session(self): + self.conversation_data.on_test_session = ~self.conversation_data.on_test_session + + async def switch_on_submit_session(self): + self.conversation_data.on_submit_session = ~self.conversation_data.on_submit_session + + async def parse_problem_set(self, json_dump): + self.conversation_data.test_title = json_dump['title'] + # print(json_dump['problems']) + self.conversation_data.problem_set.extend(json_dump['problems']) + # print(self.problem_set) + + async def get_problems(self, problem_id): + target_path = os.path.join(self.API_base, 'tests', problem_id) + # target_path = self.API_base + # print(target_path) + + session = aiohttp.ClientSession() + + async with session.get(target_path) as resp: + status = resp.status + # print(status) + response = await resp.json() + + await session.close() + return status, response + + async def register_student(self): + session = aiohttp.ClientSession() + path = self.API_base + path = os.path.join(path,'students') + async with session.post(path, json={'name':self.user_profile.student_name}) as resp: + data = await resp.json() + self.user_profile.student_ID = data['id'] + await session.close() + + async def get_student_id(self): + return self.user_profile.student_ID + + async def count_up(self): + self.conversation_data.counter = min(self.conversation_data.counter+1, len(self.conversation_data.problem_set)) + + async def count_down(self): + self.conversation_data.counter = max(self.conversation_data.counter-1, 1) + + async def get_stored_answer(self): + return self.conversation_data.answers[-1].lower() + + async def update_collected_answer(self, answer, q_id): + self.conversation_data.answers[str(self.conversation_data.counter)] = {'q_id':q_id, 'ans':answer} + await self.count_up() + + async def on_message_activity(self, turn_context: TurnContext): + # first and foremost, retreive data from memory state + self.user_profile = await self.user_state_accessor.get(turn_context, UserProfile) + self.conversation_data = await self.conversation_state_accessor.get(turn_context, ConversationData) + + # this is bad code practice with no meaning + # but I will leave it here + if turn_context.activity.text is not None: + user_input = turn_context.activity.text + else: + user_input = None + + if not self.conversation_data.on_register_complete: + await self.__send_registration_card(turn_context) + + elif (not self.conversation_data.on_test_session and not self.conversation_data.on_submit_session) and user_input[0]!='#': + await self.__send_intro_card(turn_context) + + # check test ID + elif (not self.conversation_data.on_test_session and not self.conversation_data.on_submit_session) and user_input[0]=='#': + test_id = user_input[1:] + if len(test_id) != 8: + await turn_context.send_activity("Test ID should be 8-digits number. Please re-enter the test ID.") + else: + status, to_parse = await self.get_problems(test_id) + if status == 404: + await turn_context.send_activity(f"Test ID of {test_id} is not found. Please insert the correct test ID.") + else: + await self.asign_test_ID(test_id) + await self.parse_problem_set(to_parse) + await turn_context.send_activity(f"Test titled {to_parse['title'].capitalize()} is found. There are {len(self.conversation_data.problem_set)} question(s). To submit your question, type 'submit'. Please type anything to start the test.") + await self.switch_on_test_session() + + # start test session + elif self.conversation_data.on_test_session and not self.conversation_data.on_submit_session: + if turn_context.activity.text is not None: + if turn_context.activity.text.lower() == 'submit': + await self.switch_on_test_session() + await self.switch_on_submit_session() + await self.__on_submit_activity(turn_context) + # await self.conversation_state.save_changes(turn_context) + # await self.user_state.save_changes(turn_context) + return + else: + await self.__send_question_card(turn_context) + + elif turn_context.activity.value is not None: + _answer = turn_context.activity.value['ans'] + _context = turn_context.activity.value['msg'] + _question_id = turn_context.activity.value['q_id'] + if _answer == 'BACK': + await self.count_down() + await turn_context.send_activity("Here is the previous question") + elif _answer == 'NEXT': + await self.count_up() + await turn_context.send_activity("Here is the next question") + else: + await turn_context.send_activity(f"Answered with { _context }") + await self.update_collected_answer(_answer, _question_id) + await turn_context.send_activity(f"{len(self.conversation_data.answers.keys())}/{len(self.conversation_data.problem_set)} question(s) have been answered.") + await self.__send_question_card(turn_context) + else: + await self.__send_question_card(turn_context) + + if len(self.conversation_data.answers.keys()) == len(self.conversation_data.problem_set): + print(self.conversation_data.answers.keys()) + print(len(self.conversation_data.problem_set)) + await turn_context.send_activity("All questions have been answered. Please type 'submit' for submission.") + + # start submission session + elif not self.conversation_data.on_test_session and self.conversation_data.on_submit_session: + await self.__on_submit_activity(turn_context) + + # await self.conversation_state.save_changes(turn_context) + # await self.user_state.save_changes(turn_context) + + async def on_members_added_activity( + self, + members_added: ChannelAccount, + turn_context: TurnContext + ): + for member_added in members_added: + if member_added.id != turn_context.activity.recipient.id: + await turn_context.send_activity("Hello and welcome to this test-taking chatbot! Please input your name to register.") + + async def __send_registration_card(self, turn_context: TurnContext): + if turn_context.activity.text is not None: + self.user_profile.student_name = turn_context.activity.text + card = HeroCard( + title="Your name is:", + text=f"{ self.user_profile.student_name }", + buttons=[ + CardAction(type=ActionTypes.message_back, title='Yes', value=True), + CardAction(type=ActionTypes.message_back, title='No', value=False) + ] + ) + + await turn_context.send_activity(MessageFactory.attachment(CardFactory.hero_card(card))) + + elif turn_context.activity.value: + await self.register_student() + student_id = await self.get_student_id() + await turn_context.send_activity(f"Registration complete. Welcome { self.user_profile.student_name }! Here is your student ID {student_id}.") + self.conversation_data.on_register_complete = True + await self.__send_intro_card(turn_context) + + else: + await turn_context.send_activity("Please input your name") + + async def __on_submit_activity(self, turn_context: TurnContext): + if turn_context.activity.value is None: + await self.__send_submit_card(turn_context) + elif turn_context.activity.value == 'SUBMIT': + await self.reset_and_submit() + await turn_context.send_activity("Your answer has been recorded.") + elif turn_context.activity.value == 'CANCEL': + await self.switch_on_test_session() + await self.switch_on_submit_session() + await self.__send_question_card(turn_context) + + async def __send_question_card(self, turn_context: TurnContext): + _fetch = self.conversation_data.problem_set[self.conversation_data.counter-1] + _question = _fetch['desc'] + _question_id = _fetch['id'] + _choices = _fetch['options'] + _button = [] + for _this in _choices: + _button.append(CardAction(type=ActionTypes.message_back, title=_this['value'], value={'q_id':_question_id, 'ans':_this['key'].upper(), 'msg':f"Answered with '{ _this['value'] }'."})) + if self.conversation_data.counter != 1: _button.append(CardAction(type=ActionTypes.message_back, title='Back', value={'q_id':None, 'ans':'back'.upper(),'msg':None})) + if self.conversation_data.counter != len(self.conversation_data.problem_set): _button.append(CardAction(type=ActionTypes.message_back, title='Next', value={'q_id':None, 'ans':'next'.upper(),'msg':None})) + card = HeroCard( + title=f"Question '{ self.conversation_data.counter }'.", + text=_question, + buttons=_button + ) + + return await turn_context.send_activity(MessageFactory.attachment(CardFactory.hero_card(card))) + + async def __send_submit_card(self, turn_context: TurnContext): + _keys = sorted(self.conversation_data.answers.keys()) + _text = '' + _text = '|| Student name: '+self.user_profile.student_name+' ' + for _key in _keys: + _text += f"|| { _key }. { self.conversation_data.answers[_key]['ans'] } " + if len(_keys) < len(self.conversation_data.problem_set): + _text += '|| There are question(s) you have not answered yet. Do you want to submit anyway?' + print(_text) + card = HeroCard( + title="Here are your test summary: ", + text=_text, + buttons=[ + CardAction(type=ActionTypes.message_back, title='Submit', value='submit'.upper()), + CardAction(type=ActionTypes.message_back, title='Cancel', value='cancel'.upper()) + ] + ) + + return await turn_context.send_activity(MessageFactory.attachment(CardFactory.hero_card(card))) + + async def __send_intro_card(self, turn_context: TurnContext): + card = HeroCard( + title=f"Hello { self.user_profile.student_name }!", + text="Welcome to the test-taking bot. " + "To start the test, please reply with the 8-digits test ID " + "starting with hashtag mark (e.g., #EC2A5FB5). ", + ) + + return await turn_context.send_activity( + MessageFactory.attachment(CardFactory.hero_card(card)) + ) \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..7163a79 --- /dev/null +++ b/config.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + +class DefaultConfig: + """ Bot Configuration """ + + PORT = 3978 + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/deploymentTemplates/template-with-new-rg.json b/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 0000000..3d6b8e5 --- /dev/null +++ b/deploymentTemplates/template-with-new-rg.json @@ -0,0 +1,264 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "newAppServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan. Defaults to \"westus\"." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "appServicePlanName": "[parameters('newAppServicePlanName')]", + "resourcesLocation": "[parameters('newAppServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]" + }, + "resources": [ + { + "name": "[parameters('groupName')]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('groupLocation')]", + "properties": {} + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('groupName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "name": "[variables('appServicePlanName')]", + "perSiteScaling": false, + "reserved": true, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app,linux", + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[variables('appServicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", + "value": "true" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2016-08-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/sites/', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "phpVersion": "", + "pythonVersion": "", + "nodeVersion": "", + "linuxFxVersion": "PYTHON|3.7", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "remoteDebuggingVersion": "VS2017", + "httpLoggingEnabled": true, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": false, + "appCommandLine": "gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:APP", + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": false, + "virtualDirectories": null + } + ], + "winAuthAdminState": 0, + "winAuthTenantState": 0, + "customAppPoolIdentityAdminState": false, + "customAppPoolIdentityTenantState": false, + "loadBalancing": "LeastRequests", + "routingRules": [], + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "vnetName": "", + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ], + "outputs": {} + } + } + } + ] +} \ No newline at end of file diff --git a/deploymentTemplates/template-with-preexisting-rg.json b/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 0000000..b79ffa4 --- /dev/null +++ b/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,242 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "F0", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2016-09-01", + "name": "[variables('servicePlanName')]", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "name": "[variables('servicePlanName')]", + "perSiteScaling": false, + "reserved": true, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2016-08-01", + "name": "[variables('webAppName')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" + ], + "kind": "app,linux", + "properties": { + "enabled": true, + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "reserved": true, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": false, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "siteConfig": { + "appSettings": [ + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + }, + { + "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", + "value": "true" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2016-08-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "phpVersion": "", + "pythonVersion": "", + "nodeVersion": "", + "linuxFxVersion": "PYTHON|3.7", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "remoteDebuggingVersion": "VS2017", + "httpLoggingEnabled": true, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": false, + "appCommandLine": "gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:APP", + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": false, + "virtualDirectories": null + } + ], + "winAuthAdminState": 0, + "winAuthTenantState": 0, + "customAppPoolIdentityAdminState": false, + "customAppPoolIdentityTenantState": false, + "loadBalancing": "LeastRequests", + "routingRules": [], + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "vnetName": "", + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..23423f8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +botbuilder-integration-aiohttp>=4.11.0 diff --git a/state_management/__init__.py b/state_management/__init__.py new file mode 100644 index 0000000..d3b5957 --- /dev/null +++ b/state_management/__init__.py @@ -0,0 +1,4 @@ +from .conversation_data import ConversationData +from .user_profile import UserProfile + +__all__ = ["ConversationData", "UserProfile"] \ No newline at end of file diff --git a/state_management/__pycache__/__init__.cpython-37.pyc b/state_management/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..fd8c72a Binary files /dev/null and b/state_management/__pycache__/__init__.cpython-37.pyc differ diff --git a/state_management/__pycache__/conversation_data.cpython-37.pyc b/state_management/__pycache__/conversation_data.cpython-37.pyc new file mode 100644 index 0000000..f64d020 Binary files /dev/null and b/state_management/__pycache__/conversation_data.cpython-37.pyc differ diff --git a/state_management/__pycache__/user_profile.cpython-37.pyc b/state_management/__pycache__/user_profile.cpython-37.pyc new file mode 100644 index 0000000..2296e97 Binary files /dev/null and b/state_management/__pycache__/user_profile.cpython-37.pyc differ diff --git a/state_management/conversation_data.py b/state_management/conversation_data.py new file mode 100644 index 0000000..89a5ade --- /dev/null +++ b/state_management/conversation_data.py @@ -0,0 +1,15 @@ +class ConversationData: + def __init__(self, channel_id=None, test_id=None, test_title=None, already_welcome=False, + on_test_session=False, on_submit_session=False, on_register_complete=False, on_complete_answer=False, + counter=1, problem_set=[], answers={}): + self.channel_id = channel_id + self.test_id = test_id + self.test_title = test_title + self.on_test_session = on_test_session + self.on_submit_session = on_submit_session + self.on_register_complete = on_register_complete + self.on_complete_answer = on_complete_answer + self.counter = counter + self.problem_set = problem_set + self.answers = answers + self.already_welcome = already_welcome \ No newline at end of file diff --git a/state_management/user_profile.py b/state_management/user_profile.py new file mode 100644 index 0000000..257dc8f --- /dev/null +++ b/state_management/user_profile.py @@ -0,0 +1,4 @@ +class UserProfile: + def __init__(self, student_name=None, student_id=None): + self.student_name = student_name + self.student_id = student_id \ No newline at end of file