From cec78386961199cdd5273032ebb6e1bdc8730723 Mon Sep 17 00:00:00 2001 From: Resindra Aji Date: Fri, 29 Jan 2021 23:29:13 +0900 Subject: [PATCH] another restructure --- .DS_Store | Bin 8196 -> 10244 bytes README.md | 42 +++ __init__.py | 0 __pycache__/bot.cpython-37.pyc | Bin 0 -> 9629 bytes __pycache__/config.cpython-37.pyc | Bin 0 -> 500 bytes app.py | 94 ++++++ bot.py | 295 ++++++++++++++++++ config.py | 12 + deploymentTemplates/template-with-new-rg.json | 264 ++++++++++++++++ .../template-with-preexisting-rg.json | 242 ++++++++++++++ requirements.txt | 1 + state_management/__init__.py | 4 + .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 320 bytes .../conversation_data.cpython-37.pyc | Bin 0 -> 801 bytes .../__pycache__/user_profile.cpython-37.pyc | Bin 0 -> 508 bytes state_management/conversation_data.py | 15 + state_management/user_profile.py | 4 + 17 files changed, 973 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __pycache__/bot.cpython-37.pyc create mode 100644 __pycache__/config.cpython-37.pyc create mode 100644 app.py create mode 100644 bot.py create mode 100644 config.py create mode 100644 deploymentTemplates/template-with-new-rg.json create mode 100644 deploymentTemplates/template-with-preexisting-rg.json create mode 100644 requirements.txt create mode 100644 state_management/__init__.py create mode 100644 state_management/__pycache__/__init__.cpython-37.pyc create mode 100644 state_management/__pycache__/conversation_data.cpython-37.pyc create mode 100644 state_management/__pycache__/user_profile.cpython-37.pyc create mode 100644 state_management/conversation_data.py create mode 100644 state_management/user_profile.py diff --git a/.DS_Store b/.DS_Store index ae0f6236bfacc153463d5c07d29ebe66cfd0a908..cbd669df8eb06b3cf6a93ddeb182a7934163d35e 100644 GIT binary patch literal 10244 zcmeHMYit}>6+WNiI5U%^9w$z`>tvH9t{cbA!+F(7n}qeNiD^PY?8Hu!cC(LZJg}bG z?9AG6jEk#+giuL&H3?54K%kQFr%(_G2@n!3w19vhLQ#$zASe9!9uX%i1*B9OC6N>hHPT0I~zMX-VX_oSRAK+%#+Oq(TeJj#uO% zU!$z@G({Oo@i|78cu$qpyQ-`jSU1ltzfUSAO|$4(M=jQ~C#-(f_C{@folrBO zUsyMpCc^S*e+RmB#*=3GzKV(}4 zy$O%#!%k}4x@O+ixO8PpBr;k|+h>cmKV`OQnk-$lI%1s4a+8PLtgyea%NUn?QGv+L`Ei* z$ISbJ!d<(K$Y{nLALlN6P1qO|@7))PJSbW|nDYF?dCTEmJsULN%e^`w>Uy$S(I>4f z(V@Pr-j?VJzOqJ9o+1f4K&N^s+j4sgQ^ilc@U8Se!>+}pgOV82s^dowKeoeoj z-_q~s&-60A0!9uDtV9bUcmwW02R5J++p!CG;~wn60Pe#fycvfviql9VgDl1|fhnBF zG~S61;t5>DC43T}#}!<~7w~0#MfIz$gvr66pMk(P`KC4|gUMGnQVWx<1I@<%*9Vh7 zUm}3mv}}3viZyNP@9f%n_Z!O}vg*^#L02W9B#IPFBz#KnP>^M~Mq5`IvH5_0i~(nz z(*$bFMc1Tuymg(%KruD>XtcNLI-^?}BNaTW#mn8CxC#UpqW zkKqE|j}I^mKZK9t6AEEv;2j7M7YKZ=Ht;5f3Qo>-&Ty@OY=dHtTC7u-;`&Nn)0u5t z=E8@Vch1zZkgRjLf(bRs@+3`DaUT6At6Eqt_4AbDcBW{YZ5`e>A7(kuQ$5F==kvW+ z#dYE>m95_7TPpbPJEbXD!GGFo#eX{U--_vJroZm43i|85f&MP?IQf{D{scYV&Yf4^R zvo<;pW45i0Mk@mxQD%-J`XT*T2CrYy@99tU68(+-PA{VYjWTepMVxtZrwm&O^vJL^h@%{`j&aC3$sB3n z48}NQP2!wnN)f8wi}!J``UpOar|=~_%}n_Xd>7xtb9f#<#S8d_#4?q`*CF*>y5hxG z1?Cc$K3EIPv3vI${jV#S=TNH3_R1f*>f2U=>|e6z9PU+g#+0h4vpRDu%>Z+e%EScQ z#p1CNrOL<=oiW2)AoD=Mznt~O%!M-J6OWwn8D_oA3B@DldNH#>rheja z@4hmRN?tYEWBs~j;-93d&^a$QpX5C5Tl9TK<#l?IGq^v{UpaScz!J{g)-W2|a68(u z8C$Rw+pq(Bu@4HF2RVBabb6F=`GBO;6yA!Qq|-d-anq7c-+_1IJ&e!`cpR7UVMgi4 zm{329D^-}R&gHJ&JSMAXR;QyhKt0q?hgfcAI?K)_F}aAWBr%f;Om%0OI&FDZB14a@ zeL7dY;DcCB&b9YgPZ4I4+es*;c{9msB{{e{) Bo5TPB delta 146 zcmZn(XmOBWU|?W$DortDU;r^WfEYvza8E20o2aKK$_A1L@);PC8R8j|81flPJaaY{ zPGg_gz`L29gN1{UZSsCWvCX>#%-JUUiyKVllK(rgp;3?-s1FDvxPgQ#$h?h(-gK7UF^mXpbpXt?A0q$& 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 0000000000000000000000000000000000000000..8ac509ffb6f81e556badf1fbea8d0ece9a280412 GIT binary patch literal 9629 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 GIq( 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 0000000000000000000000000000000000000000..fd8c72ac5c13e3cdf2c7a023c7efd94c6f301527 GIT binary patch literal 320 zcmYjMJ5Izf5RH>8iy$k3xWE?Mq`?(o%5+ zD#k027|G9XX5M?!+^$w985Mk7JiXEXnVP@KiQLc&8v>CDrZdA@#uCqUZi==bUWjtc z+fpo=tUmf;B(2$ecIWJi^Z}!Cc8d{dcDf7FKYBMPEq%)N1;InTn9uuvY7itp?B{AC z2kd_>Nj)zCP-}qq%r8?!g71!}=)lp-pseu7x2~Uzv@!5T>S5i)X&Csu!BJVs`)7=u ti_l?EJwG5&&qKs0!Jx%aCNl?6Rz-lz{p);uWt^C_yiON}lAvN${sXJ8Sx^7~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f64d020226cdbe403df28e447719a90a7289bdce GIT binary patch literal 801 zcmZXS!H&}~5J2rDZPK(=cULPUZd@y&%?}{7Vpof}^ipx@#kldbL?lkJ9af8cW50mE z;Fo+t;>4L7CuULwRp8M)kH?;|Gm}^IdCqWron5~B9x?WV7Q=|qxJI=Xh}TRp#UD|> zL>s1}HCx6d`u~n8TGy!dIRe;*E4GOg?InY zEYv$g?Jx;pM3@uCgp`mGCIo7zp>7ze+BaqvIQ{-mytP>2LK?7@Rz?=LZP_)Toh{aw z{@L1fyIt|7k^4$RfhBR<)`_jORax{DxGt=d4n!lh+yey{SUl$c%jc*6HvVdeTqcx7%Lar9xe!gOx|-~n5Fa~PA9u_-rtUxR1ZU)<--pyv di84;o`&P^2d~%-ZV<;$OFOJu|q{v^W><_H*&FugH literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2296e97267212505bf388b21fc3d58c72d829195 GIT binary patch literal 508 zcmYjNOHRWu5Vf7ORHc;=Al7WNpuPY?2#^q)2&rO|&EUpPiIh*Vodu{ndJHbW8MsNd z9DyAxW)ijPNbilup7$n`o9T4SFfP7No` zC66P~PMmMFsXSDwH0^j>5KA<+QUM*Je3^-*3x<94V0=?g-sG@(d2k6r+vPVO@<1_%_Xx@l- YbsY71ub2LTB|$s7>_=RY>KDQO0X~*^i2wiq literal 0 HcmV?d00001 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