Team Ai
Apppublic

TangibleAI/mathtext-fastapi

sourceHugging Faceagpl-3.0updated 3y agoView on Hugging Face
1likes
conversation_manager.py442 linesDownload Raw Back to mathtext_fastapi
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