TangibleAI/mathtext-fastapi
1
1import base642import dill3import os4import json5import jsonpickle6import pickle7import random8import requests9 10from dotenv import load_dotenv11from mathtext_fastapi.nlu import evaluate_message_with_nlu12from mathtext_fastapi.math_quiz_fsm import MathQuizFSM13from mathtext_fastapi.math_subtraction_fsm import MathSubtractionFSM14from supabase import create_client15from transitions import Machine16 17from mathactive.generators import start_interactive_math18from mathactive.hints import generate_hint19 20load_dotenv()21 22SUPA = create_client(23 os.environ.get('SUPABASE_URL'),24 os.environ.get('SUPABASE_KEY')25)26 27 28def create_text_message(message_text, whatsapp_id):29 """ Fills a template with input values to send a text message to Whatsapp30 31 Inputs32 - message_text: str - the content that the message should display33 - whatsapp_id: str - the message recipient's phone number34 35 Outputs36 - message_data: dict - a preformatted template filled with inputs37 """38 message_data = {39 "preview_url": False,40 "recipient_type": "individual",41 "to": whatsapp_id,42 # FIXME: Better to use "message_type" (but be careful with refactor)43 "type": "text",44 "text": {45 "body": message_text46 }47 }48 return message_data49 50 51def create_button_objects(button_options):52 """ Creates a list of button objects using the input values53 Input54 - button_options: list - a list of text to be displayed in buttons55 56 Output57 - button_arr: list - preformatted button objects filled with the inputs58 59 NOTE: Not fully implemented and tested60 """61 button_arr = []62 for option in button_options:63 button_choice = {64 "type": "reply",65 "reply": {66 "id": "inquiry-yes",67 "title": option['text']68 }69 }70 button_arr.append(button_choice)71 return button_arr72 73 74def create_interactive_message(message_text, button_options, whatsapp_id):75 """ Fills a template to create a button message for Whatsapp76 77 * NOTE: Not fully implemented and tested78 * NOTE/TODO: It is possible to create other kinds of messages79 with the 'interactive message' template80 * Documentation:81 https://whatsapp.turn.io/docs/api/messages#interactive-messages82 83 Inputs84 - message_text: str - the content that the message should display85 - button_options: list - what each button option should display86 - whatsapp_id: str - the message recipient's phone number87 """88 button_arr = create_button_objects(button_options)89 90 data = {91 "to": whatsapp_id,92 "type": "interactive",93 "interactive": {94 "type": "button",95 # "header": { },96 "body": {97 "text": message_text98 },99 # "footer": { },100 "action": {101 "buttons": button_arr102 }103 }104 }105 return data106 107 108def pickle_and_encode_state_machine(state_machine):109 dump = pickle.dumps(state_machine)110 dump_encoded = base64.b64encode(dump).decode('utf-8')111 return dump_encoded112 113 114def manage_math_quiz_fsm(user_message, contact_uuid, type):115 fsm_check = SUPA.table('state_machines').select("*").eq(116 "contact_uuid",117 contact_uuid118 ).execute()119 120 # This doesn't allow for when one FSM is present and the other is empty121 """122 1123 data=[] count=None124 125 2126 data=[{'id': 29, 'contact_uuid': 'j43hk26-2hjl-43jk-hnk2-k4ljl46j0ds09', 'addition3': None, 'subtraction': None, 'addition': 127 128 - but problem is there is no subtraction , but it's assuming there's a subtration129 130 131 Cases132 - make a completely new record133 - update an existing record with an existing FSM134 - update an existing record without an existing FSM135 """136 137 138 # Make a completely new entry139 if fsm_check.data == []:140 # FIXME: Try not to use the Python reserved keyword `type` as a variable name141 # It's better to use `kind` or `convo_type` or `convo_name`142 # And the variable `type` is not defined here so I don't understand how this is working at all.143 if type == 'addition':144 math_quiz_state_machine = MathQuizFSM()145 else:146 math_quiz_state_machine = MathSubtractionFSM()147 messages = [math_quiz_state_machine.response_text]148 dump_encoded = pickle_and_encode_state_machine(math_quiz_state_machine)149 150 SUPA.table('state_machines').insert({151 'contact_uuid': contact_uuid,152 f'{type}': dump_encoded153 }).execute()154 # Update an existing record with a new state machine155 elif not fsm_check.data[0][type]:156 if type == 'addition':157 math_quiz_state_machine = MathQuizFSM()158 else:159 math_quiz_state_machine = MathSubtractionFSM()160 messages = [math_quiz_state_machine.response_text]161 dump_encoded = pickle_and_encode_state_machine(math_quiz_state_machine)162 163 SUPA.table('state_machines').update({164 f'{type}': dump_encoded165 }).eq(166 "contact_uuid", contact_uuid167 ).execute() 168 # Update an existing record with an existing state machine169 elif fsm_check.data[0][type]:170 undump_encoded = base64.b64decode(171 fsm_check.data[0][type].encode('utf-8')172 )173 math_quiz_state_machine = pickle.loads(undump_encoded)174 175 math_quiz_state_machine.student_answer = user_message176 math_quiz_state_machine.correct_answer = str(math_quiz_state_machine.correct_answer)177 messages = math_quiz_state_machine.validate_answer()178 dump_encoded = pickle_and_encode_state_machine(math_quiz_state_machine) 179 SUPA.table('state_machines').update({180 f'{type}': dump_encoded181 }).eq(182 "contact_uuid", contact_uuid183 ).execute()184 return messages185 186 187def use_quiz_module_approach(user_message, context_data):188 print("USER MESSAGE")189 print(user_message)190 print("=======================")191 if user_message == 'add':192 context_result = start_interactive_math()193 message_package = {194 'messages': [195 "Great, let's do some addition",196 "First, we'll start with single digits.",197 "Type your response as a number. For example, for '1 + 1', you'd write 2."198 ],199 'input_prompt': context_result['text'],200 'state': "addition-question-sequence"201 }202 203 elif user_message == context_data.get('right_answer'):204 context_result = start_interactive_math(205 context_data['number_correct'],206 context_data['number_incorrect'],207 context_data['level']208 )209 message_package = {210 'messages': [211 "That's right, great!",212 ],213 'input_prompt': context_result['text'],214 'state': "addition-question-sequence"215 }216 else:217 context_result = generate_hint(218 context_data['question_numbers'],219 context_data['right_answer'],220 context_data['number_correct'],221 context_data['number_incorrect'],222 context_data['level'],223 context_data['hints_used']224 )225 message_package = {226 'messages': [227 context_result['text'],228 ],229 'input_prompt': context_data['text'],230 'state': "addition-question-sequence"231 }232 return message_package, context_result233 234 235def return_next_conversational_state(context_data, user_message, contact_uuid):236 """ Evaluates the conversation's current state to determine the next state237 238 Input239 - context_data: dict - data about the conversation's current state240 - user_message: str - the message the user sent in response to the state241 242 Output243 - message_package: dict - a series of messages and prompt to send244 """245 if context_data['user_message'] == '' and \246 context_data['state'] == 'start-conversation':247 message_package = {248 'messages': [],249 'input_prompt': "Welcome to our math practice. What would you like to try? Type add or subtract.",250 'state': "welcome-sequence"251 }252 elif context_data['state'] == 'addition-question-sequence' or \253 user_message == 'add':254 255 # Used in FSM256 # messages = manage_math_quiz_fsm(user_message, contact_uuid)257 258 # message_package, context_result = use_quiz_module_approach(user_message, context_data)259 messages = manage_math_quiz_fsm(user_message, contact_uuid, 'addition')260 261 if user_message == 'exit':262 state_label = 'exit'263 else:264 state_label = 'addition-question-sequence'265 # Used in FSM266 input_prompt = messages.pop()267 message_package = {268 'messages': messages,269 'input_prompt': input_prompt,270 'state': state_label271 }272 273 # Used in quiz w/ hints274 # context_data = context_result275 # message_package['state'] = state_label276 277 elif context_data['state'] == 'subtraction-question-sequence' or \278 user_message == 'subtract':279 messages = manage_math_quiz_fsm(user_message, contact_uuid, 'subtraction')280 281 if user_message == 'exit':282 state_label = 'exit'283 else:284 state_label = 'subtraction-question-sequence'285 286 input_prompt = messages.pop()287 288 message_package = {289 'messages': messages,290 'input_prompt': input_prompt,291 'state': state_label292 }293 294 # message_package = {295 # 'messages': [296 # "Time for some subtraction!",297 # "Type your response as a number. For example, for '1 - 1', you'd write 0."298 # ],299 # 'input_prompt': "Here's the first one... What's 3-1?",300 # 'state': "subtract-question-sequence"301 # }302 elif context_data['state'] == 'exit' or user_message == 'exit':303 message_package = {304 'messages': [305 "Great, thanks for practicing math today. Come back any time."306 ],307 'input_prompt': "",308 'state': "exit"309 }310 else:311 message_package = {312 'messages': [313 "Hmmm...sorry friend. I'm not really sure what to do."314 ],315 'input_prompt': "Please type add or subtract to start a math activity.",316 'state': "reprompt-menu-options"317 }318 # Used in FSM319 return message_package320 321 # Used in quiz folder approach322 # return context_result, message_package323 324 325def manage_conversation_response(data_json):326 """ Calls functions necessary to determine message and context data to send327 328 Input329 - data_json: dict - message data from Turn.io/Whatsapp330 331 Output332 - context: dict - a record of the state at a given point a conversation333 334 TODOs335 - implement logging of message336 - test interactive messages337 - review context object and re-work to use a standardized format338 - review ways for more robust error handling339 - need to make util functions that apply to both /nlu and /conversation_manager340 """341 message_data = data_json.get('message_data', '')342 context_data = data_json.get('context_data', '')343 344 whatsapp_id = message_data['author_id']345 user_message = message_data['message_body']346 contact_uuid = message_data['contact_uuid']347 348 # TODO: Need to incorporate nlu_response into wormhole by checking answers against database (spreadsheet?)349 nlu_response = evaluate_message_with_nlu(message_data)350 351 if context_data['state'] == 'addition':352 context_result, message_package = return_next_conversational_state(353 context_data,354 user_message,355 contact_uuid356 )357 else:358 message_package = return_next_conversational_state(359 context_data,360 user_message,361 contact_uuid362 )363 364 headers = {365 'Authorization': f"Bearer {os.environ.get('TURN_AUTHENTICATION_TOKEN')}",366 'Content-Type': 'application/json'367 }368 369 # Send all messages for the current state before a user input prompt (text/button input request)370 for message in message_package['messages']:371 data = create_text_message(message, whatsapp_id)372 373 print("data")374 print(data)375 376 r = requests.post(377 f'https://whatsapp.turn.io/v1/messages',378 data=json.dumps(data),379 headers=headers380 )381 382 # Update the context object with the new state of the conversation383 if context_data['state'] == 'addition':384 context = {385 "context": {386 "user": whatsapp_id,387 "state": message_package['state'],388 "bot_message": message_package['input_prompt'],389 "user_message": user_message,390 "type": 'ask',391 # Necessary for quiz folder approach392 "text": context_result.get('text'),393 "question_numbers": context_result.get('question_numbers'),394 "right_answer": context_result.get('right_answer'),395 "number_correct": context_result.get('number_correct'),396 "hints_used": context_result.get('hints_used'),397 }398 }399 else:400 context = {401 "context": {402 "user": whatsapp_id,403 "state": message_package['state'],404 "bot_message": message_package['input_prompt'],405 "user_message": user_message,406 "type": 'ask',407 }408 }409 410 return context411 412 # data = {413 # "to": whatsapp_id,414 # "type": "interactive",415 # "interactive": {416 # "type": "button",417 # # "header": { },418 # "body": {419 # "text": "Did I answer your question?"420 # },421 # # "footer": { },422 # "action": {423 # "buttons": [424 # {425 # "type": "reply",426 # "reply": {427 # "id": "inquiry-yes",428 # "title": "Yes"429 # }430 # },431 # {432 # "type": "reply",433 # "reply": {434 # "id": "inquiry-no",435 # "title": "No"436 # }437 # }438 # ]439 # }440 # }441 # }442 