import base64 import copy import dill import os import json import jsonpickle import pickle import random import requests import mathtext_fastapi.global_state_manager as gsm from dotenv import load_dotenv from mathtext_fastapi.nlu import evaluate_message_with_nlu from mathtext_fastapi.math_quiz_fsm import MathQuizFSM from mathtext_fastapi.math_subtraction_fsm import MathSubtractionFSM from supabase import create_client from transitions import Machine from mathactive.generators import start_interactive_math from mathactive.hints import generate_hint from mathactive.microlessons import num_one load_dotenv() SUPA = create_client( os.environ.get('SUPABASE_URL'), os.environ.get('SUPABASE_KEY') ) def pickle_and_encode_state_machine(state_machine): dump = pickle.dumps(state_machine) dump_encoded = base64.b64encode(dump).decode('utf-8') return dump_encoded def manage_math_quiz_fsm(user_message, contact_uuid, type): fsm_check = SUPA.table('state_machines').select("*").eq( "contact_uuid", contact_uuid ).execute() # This doesn't allow for when one FSM is present and the other is empty """ 1 data=[] count=None 2 data=[{'id': 29, 'contact_uuid': 'j43hk26-2hjl-43jk-hnk2-k4ljl46j0ds09', 'addition3': None, 'subtraction': None, 'addition': - but problem is there is no subtraction , but it's assuming there's a subtration Cases - make a completely new record - update an existing record with an existing FSM - update an existing record without an existing FSM """ print("MATH QUIZ FSM ACTIVITY") print("user_message") print(user_message) # Make a completely new entry if fsm_check.data == []: if type == 'addition': math_quiz_state_machine = MathQuizFSM() else: math_quiz_state_machine = MathSubtractionFSM() messages = [math_quiz_state_machine.response_text] dump_encoded = pickle_and_encode_state_machine(math_quiz_state_machine) SUPA.table('state_machines').insert({ 'contact_uuid': contact_uuid, f'{type}': dump_encoded }).execute() # Update an existing record with a new state machine elif not fsm_check.data[0][type]: if type == 'addition': math_quiz_state_machine = MathQuizFSM() else: math_quiz_state_machine = MathSubtractionFSM() messages = [math_quiz_state_machine.response_text] dump_encoded = pickle_and_encode_state_machine(math_quiz_state_machine) SUPA.table('state_machines').update({ f'{type}': dump_encoded }).eq( "contact_uuid", contact_uuid ).execute() # Update an existing record with an existing state machine elif fsm_check.data[0][type]: undump_encoded = base64.b64decode( fsm_check.data[0][type].encode('utf-8') ) math_quiz_state_machine = pickle.loads(undump_encoded) math_quiz_state_machine.student_answer = user_message math_quiz_state_machine.correct_answer = str(math_quiz_state_machine.correct_answer) messages = math_quiz_state_machine.validate_answer() dump_encoded = pickle_and_encode_state_machine(math_quiz_state_machine) SUPA.table('state_machines').update({ f'{type}': dump_encoded }).eq( "contact_uuid", contact_uuid ).execute() return messages def retrieve_microlesson_content(context_data, user_message, microlesson, contact_uuid): # TODO: This is being filtered by both the local and global states, so not changing if microlesson == 'addition': messages = manage_math_quiz_fsm(user_message, contact_uuid, 'addition') if user_message == 'exit': state_label = 'exit' else: state_label = 'addition-question-sequence' input_prompt = messages.pop() message_package = { 'messages': messages, 'input_prompt': input_prompt, 'state': state_label } elif context_data['local_state'] == 'addition2' or microlesson == 'addition2': if user_message == 'harder' or user_message == 'easier': user_message = '' message_package = num_one.process_user_message(contact_uuid, user_message) message_package['state'] = 'addition2' message_package['input_prompt'] = '?' elif context_data['local_state'] == 'subtraction-question-sequence' or \ user_message == 'subtract' or \ microlesson == 'subtraction': messages = manage_math_quiz_fsm(user_message, contact_uuid, 'subtraction') if user_message == 'exit': state_label = 'exit' else: state_label = 'subtraction-question-sequence' input_prompt = messages.pop() message_package = { 'messages': messages, 'input_prompt': input_prompt, 'state': state_label } print("MICROLESSON CONTENT RESPONSE") print(message_package) return message_package curriculum_lookup_table = { 'N1.1.1_G1': 'addition', 'N1.1.1_G2': 'addition2', 'N1.1.2_G1': 'subtraction' } def lookup_local_state(next_state): microlesson = curriculum_lookup_table[next_state] return microlesson def create_text_message(message_text, whatsapp_id): """ Fills a template with input values to send a text message to Whatsapp Inputs - message_text: str - the content that the message should display - whatsapp_id: str - the message recipient's phone number Outputs - message_data: dict - a preformatted template filled with inputs """ message_data = { "preview_url": False, "recipient_type": "individual", "to": whatsapp_id, "type": "text", "text": { "body": message_text } } return message_data def manage_conversation_response(data_json): """ Calls functions necessary to determine message and context data """ print("V2 ENDPOINT") # whatsapp_id = data_json['author_id'] message_data = data_json['message_data'] context_data = data_json['context_data'] whatsapp_id = message_data['author_id'] user_message = message_data['message_body'] print("MESSAGE DATA") print(message_data) print("CONTEXT DATA") print(context_data) print("=================") # nlu_response = evaluate_message_with_nlu(message_data) # context_data = { # 'contact_uuid': 'abcdefg', # 'current_state': 'N1.1.1_G2', # 'user_message': '1', # 'local_state': '' # } print("STEP 1") print(data_json) print(f"1: {context_data['current_state']}") if not context_data['current_state']: context_data['current_state'] = 'N1.1.1_G1' print(f"2: {context_data['current_state']}") curriculum_copy = copy.deepcopy(gsm.curriculum) curriculum_copy.state = context_data['current_state'] print("STEP 2") if user_message == 'easier': curriculum_copy.left() next_state = curriculum_copy.state elif user_message == 'harder': curriculum_copy.right() next_state = curriculum_copy.state else: next_state = context_data['current_state'] print("next_state") print(next_state) print("STEP 3") microlesson = lookup_local_state(next_state) print("microlesson") print(microlesson) microlesson_content = retrieve_microlesson_content(context_data, user_message, microlesson, context_data['contact_uuid']) headers = { 'Authorization': f"Bearer {os.environ.get('TURN_AUTHENTICATION_TOKEN')}", 'Content-Type': 'application/json' } # Send all messages for the current state before a user input prompt (text/button input request) for message in microlesson_content['messages']: data = create_text_message(message, whatsapp_id) # print("data") # print(data) r = requests.post( f'https://whatsapp.turn.io/v1/messages', data=json.dumps(data), headers=headers ) print("STEP 4") # combine microlesson content and context_data object updated_context = { "context": { "contact_id": whatsapp_id, "contact_uuid": context_data['contact_uuid'], "current_state": next_state, "local_state": microlesson_content['state'], "bot_message": microlesson_content['input_prompt'], "user_message": user_message, "type": 'ask' } } print(updated_context) return updated_context