diff --git a/kairon/chat/handlers/channels/whatsapp.py b/kairon/chat/handlers/channels/whatsapp.py index 2239af335..899d4fcdf 100644 --- a/kairon/chat/handlers/channels/whatsapp.py +++ b/kairon/chat/handlers/channels/whatsapp.py @@ -40,7 +40,14 @@ async def message( # so quick reply should be checked first if message.get("type") == "interactive": interactive_type = message.get("interactive").get("type") - text = message["interactive"][interactive_type]["id"] + if interactive_type == "nfm_reply": + logger.debug(message["interactive"][interactive_type]) + response_json = json.loads(message["interactive"][interactive_type]['response_json']) + response_json.update({"type": interactive_type}) + entity = json.dumps({"flow_reply": response_json}) + text = f"/k_interactive_msg{entity}" + else: + text = message["interactive"][interactive_type]["id"] elif message.get("type") == "text": text = message["text"]['body'] elif message.get("type") == "button": diff --git a/kairon/shared/constants.py b/kairon/shared/constants.py index 8d00869ac..5f18109dd 100644 --- a/kairon/shared/constants.py +++ b/kairon/shared/constants.py @@ -142,6 +142,7 @@ class KaironSystemSlots(str, Enum): document = "document" doc_url = "doc_url" order = "order" + flow_reply = "flow_reply" class VectorEmbeddingsDatabases(str, Enum): diff --git a/tests/integration_test/chat_service_test.py b/tests/integration_test/chat_service_test.py index 94341df3d..a4ad18f4a 100644 --- a/tests/integration_test/chat_service_test.py +++ b/tests/integration_test/chat_service_test.py @@ -1591,6 +1591,82 @@ def _mock_validate_hub_signature(*args, **kwargs): assert whatsapp_msg_handler.call_args[0][4] == bot +@responses.activate +def test_whatsapp_valid_flows_message_request(): + responses.reset() + + def _mock_validate_hub_signature(*args, **kwargs): + return True + + with patch.object(MessengerHandler, "validate_hub_signature", _mock_validate_hub_signature): + with mock.patch("kairon.chat.handlers.channels.whatsapp.Whatsapp._handle_user_message", + autospec=True) as whatsapp_msg_handler: + request_json = { + 'object': 'whatsapp_business_account', + 'entry': [{ + 'id': '147142368486217', + 'changes': [{ + 'value': { + 'messaging_product': 'whatsapp', + 'metadata': { + 'display_phone_number': '918657011111', + 'phone_number_id': '142427035629239' + }, + 'contacts': [{ + 'profile': { + 'name': 'Mahesh' + }, + 'wa_id': '919515991111' + }], + 'messages': [{ + 'context': { + 'from': '918657011111', + 'id': 'wamid.HBgMOTE5NTE1OTkxNjg1FQIAERgSMjVGRjYwODI3RkMyOEQ0NUM1AA==' + }, + 'from': '919515991111', + 'id': 'wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggQTRBQUYyODNBQkMwNEIzRDQ0MUI1ODkyMTE2NTMA', + 'timestamp': '1703257297', + 'type': 'interactive', + 'interactive': { + 'type': 'nfm_reply', + 'nfm_reply': { + 'response_json': '{"flow_token":"AQBBBBBCS5FpgQ_cAAAAAD0QI3s.","firstName":"Mahesh ","lastName":"Sattala ","pincode":"523456","district":"Bangalore ","houseNumber":"5-6","dateOfBirth":"1703257240046","source":"SOCIAL_MEDIA","landmark":"HSR Layout ","email":"maheshsattala@gmail.com"}', + 'body': 'Sent', + 'name': 'flow' + } + } + }] + }, + 'field': 'messages' + }] + }] + } + response = client.post( + f"/api/bot/whatsapp/{bot}/{token}", + headers={"hub.verify_token": "valid"}, + json=request_json + ) + actual = response.json() + assert actual == 'success' + assert len(whatsapp_msg_handler.call_args[0]) == 5 + assert whatsapp_msg_handler.call_args[0][1] == '/k_interactive_msg{\"flow_reply\": {\"flow_token\": \"AQBBBBBCS5FpgQ_cAAAAAD0QI3s.\", \"firstName\": \"Mahesh \", \"lastName\": \"Sattala \", \"pincode\": \"523456\", \"district\": \"Bangalore \", \"houseNumber\": \"5-6\", \"dateOfBirth\": \"1703257240046\", \"source\": \"SOCIAL_MEDIA\", \"landmark\": \"HSR Layout \", \"email\": \"maheshsattala@gmail.com\", \"type\": \"nfm_reply\"}}' + assert whatsapp_msg_handler.call_args[0][2] == '919515991111' + metadata = whatsapp_msg_handler.call_args[0][3] + metadata.pop("timestamp") + assert metadata == { + 'context': {'from': '918657011111', 'id': 'wamid.HBgMOTE5NTE1OTkxNjg1FQIAERgSMjVGRjYwODI3RkMyOEQ0NUM1AA=='}, + 'from': '919515991111', 'id': 'wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggQTRBQUYyODNBQkMwNEIzRDQ0MUI1ODkyMTE2NTMA', + 'type': 'interactive', + 'interactive': { + 'type': 'nfm_reply', 'nfm_reply': { + 'response_json': '{"flow_token":"AQBBBBBCS5FpgQ_cAAAAAD0QI3s.","firstName":"Mahesh ","lastName":"Sattala ","pincode":"523456","district":"Bangalore ","houseNumber":"5-6","dateOfBirth":"1703257240046","source":"SOCIAL_MEDIA","landmark":"HSR Layout ","email":"maheshsattala@gmail.com"}', + 'body': 'Sent', 'name': 'flow'}}, + 'is_integration_user': True, 'bot': bot, 'account': 1, 'channel_type': 'whatsapp', + 'bsp_type': 'meta', 'tabname': 'default', 'display_phone_number': '918657011111', + 'phone_number_id': '142427035629239'} + assert whatsapp_msg_handler.call_args[0][4] == bot + + @responses.activate def test_whatsapp_valid_statuses_with_sent_request(): from kairon.shared.chat.data_objects import ChannelLogs diff --git a/tests/integration_test/services_test.py b/tests/integration_test/services_test.py index c719ea583..cf4885af5 100644 --- a/tests/integration_test/services_test.py +++ b/tests/integration_test/services_test.py @@ -2911,7 +2911,7 @@ def test_list_entities_empty(): ) actual = response.json() assert actual["error_code"] == 0 - assert len(actual['data']) == 8 + assert len(actual['data']) == 9 assert actual["success"] @@ -3036,7 +3036,8 @@ def test_list_entities(): assert actual["error_code"] == 0 assert {e['name'] for e in actual["data"]} == {'bot', 'file', 'category', 'file_text', 'ticketid', 'file_error', 'priority', 'requested_slot', 'fdresponse', 'kairon_action_response', - 'audio', 'image', 'doc_url', 'document', 'video', 'order'} + 'audio', 'image', 'doc_url', 'document', 'video', 'order', + 'flow_reply'} assert actual["success"] @@ -3437,7 +3438,7 @@ def test_get_slots(): ) actual = response.json() assert "data" in actual - assert len(actual["data"]) == 15 + assert len(actual["data"]) == 16 assert actual["success"] assert actual["error_code"] == 0 assert Utility.check_empty_string(actual["message"]) diff --git a/tests/unit_test/data_processor/data_processor_test.py b/tests/unit_test/data_processor/data_processor_test.py index bb99361bd..f9c4a0ab7 100644 --- a/tests/unit_test/data_processor/data_processor_test.py +++ b/tests/unit_test/data_processor/data_processor_test.py @@ -1035,7 +1035,7 @@ async def test_save_from_path_yml(self): assert len(list(Intents.objects(bot="test_load_yml", user="testUser", use_entities=False))) == 5 assert len(list(Intents.objects(bot="test_load_yml", user="testUser", use_entities=True))) == 27 assert len( - list(Slots.objects(bot="test_load_yml", user="testUser", influence_conversation=True, status=True))) == 7 + list(Slots.objects(bot="test_load_yml", user="testUser", influence_conversation=True, status=True))) == 8 assert len( list(Slots.objects(bot="test_load_yml", user="testUser", influence_conversation=False, status=True))) == 9 multiflow_stories = processor.load_multiflow_stories_yaml(bot='test_load_yml') @@ -1455,11 +1455,11 @@ async def test_upload_case_insensitivity(self): domain = processor.load_domain("test_upload_case_insensitivity") assert all(slot.name in ['session_started_metadata', 'requested_slot', 'application_name', 'bot', 'email_id', 'location', 'user', 'kairon_action_response', 'image', 'video', 'audio', 'doc_url', - 'document', 'order'] for slot in domain.slots) + 'document', 'order', 'flow_reply'] for slot in domain.slots) assert list(domain.templates.keys()) == ['utter_please_rephrase', 'utter_greet', 'utter_goodbye', 'utter_default'] assert domain.entities == ['user', 'location', 'email_id', 'application_name', 'bot', 'kairon_action_response', - 'order', 'image', 'audio', 'video', 'document', 'doc_url'] + 'order', 'image', 'audio', 'video', 'document', 'doc_url', 'flow_reply'] assert domain.forms == {'ask_user': {'required_slots': {'user': [{'type': 'from_entity', 'entity': 'user'}], 'email_id': [ {'type': 'from_entity', 'entity': 'email_id'}]}}, @@ -1563,8 +1563,8 @@ async def test_load_from_path_yml_training_files(self): assert story_graph.story_steps[15].events[2].entities[0]['entity'] == 'fdresponse' domain = processor.load_domain("test_load_from_path_yml_training_files") assert isinstance(domain, Domain) - assert domain.slots.__len__() == 18 - assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 7 + assert domain.slots.__len__() == 19 + assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 8 assert len([slot for slot in domain.slots if slot.influence_conversation is False]) == 11 assert domain.intent_properties.__len__() == 32 assert len([intent for intent in domain.intent_properties.keys() if @@ -1572,7 +1572,7 @@ async def test_load_from_path_yml_training_files(self): assert len([intent for intent in domain.intent_properties.keys() if not domain.intent_properties.get(intent)['used_entities']]) == 5 assert domain.templates.keys().__len__() == 29 - assert domain.entities.__len__() == 17 + assert domain.entities.__len__() == 18 assert domain.forms.__len__() == 2 assert domain.forms.__len__() == 2 assert domain.forms['ticket_attributes_form'] == { @@ -1636,9 +1636,9 @@ async def test_load_from_path_all_scenario(self): assert story_graph.story_steps[15].events[2].entities[0]['entity'] == 'fdresponse' domain = processor.load_domain("all") assert isinstance(domain, Domain) - assert domain.slots.__len__() == 17 + assert domain.slots.__len__() == 18 assert domain.templates.keys().__len__() == 27 - assert domain.entities.__len__() == 16 + assert domain.entities.__len__() == 17 assert domain.forms.__len__() == 2 assert domain.forms['ticket_attributes_form'] == {'required_slots': {}} assert isinstance(domain.forms, dict) @@ -1682,9 +1682,9 @@ async def test_load_from_path_all_scenario_append(self): assert story_graph.story_steps[15].events[2].entities[0]['entity'] == 'fdresponse' domain = processor.load_domain("all") assert isinstance(domain, Domain) - assert domain.slots.__len__() == 17 + assert domain.slots.__len__() == 18 assert domain.templates.keys().__len__() == 27 - assert domain.entities.__len__() == 16 + assert domain.entities.__len__() == 17 assert domain.forms.__len__() == 2 assert isinstance(domain.forms, dict) assert domain.user_actions.__len__() == 40 @@ -1710,10 +1710,10 @@ def test_load_domain(self): processor = MongoProcessor() domain = processor.load_domain("tests") assert isinstance(domain, Domain) - assert domain.slots.__len__() == 9 + assert domain.slots.__len__() == 10 assert [s.name for s in domain.slots if s.name == 'kairon_action_response' and s.value is None] assert domain.templates.keys().__len__() == 11 - assert domain.entities.__len__() == 8 + assert domain.entities.__len__() == 9 assert domain.form_names.__len__() == 0 assert domain.user_actions.__len__() == 11 assert domain.intents.__len__() == 14 @@ -1960,7 +1960,7 @@ def test_add_training_example_with_entity(self): ) slots = Slots.objects(bot="tests") new_slot = slots.get(name="priority") - assert slots.__len__() == 9 + assert slots.__len__() == 10 assert new_slot.name == "priority" assert new_slot.type == "text" assert new_training_example.text == "Log a critical issue" @@ -1993,7 +1993,7 @@ def test_get_training_examples_with_entities(self): for value in actual ] ) - assert slots.__len__() == 10 + assert slots.__len__() == 11 assert new_slot.name == "ticketid" assert new_slot.type == "text" expected = ["hey", "hello", "hi", "good morning", "good evening", "hey there"] @@ -2036,7 +2036,7 @@ def test_add_entity(self): def test_get_entities(self): processor = MongoProcessor() expected = ["bot", "priority", "file_text", "ticketid", 'kairon_action_response', 'image', 'video', 'audio', - 'doc_url', 'document', 'order'] + 'doc_url', 'document', 'order', 'flow_reply'] actual = processor.get_entities("tests") assert actual.__len__() == expected.__len__() assert all(item["name"] in expected for item in actual) @@ -4690,8 +4690,8 @@ def _mock_bot_info(*args, **kwargs): assert story_graph.story_steps[15].events[2].entities[0]['entity'] == 'fdresponse' domain = mongo_processor.load_domain(bot) assert isinstance(domain, Domain) - assert domain.slots.__len__() == 18 - assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 7 + assert domain.slots.__len__() == 19 + assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 8 assert len([slot for slot in domain.slots if slot.influence_conversation is False]) == 11 assert domain.intent_properties.__len__() == 32 assert len([intent for intent in domain.intent_properties.keys() if @@ -4699,7 +4699,7 @@ def _mock_bot_info(*args, **kwargs): assert len([intent for intent in domain.intent_properties.keys() if not domain.intent_properties.get(intent)['used_entities']]) == 5 assert domain.templates.keys().__len__() == 29 - assert domain.entities.__len__() == 17 + assert domain.entities.__len__() == 18 assert domain.form_names.__len__() == 2 assert domain.user_actions.__len__() == 48 assert domain.intents.__len__() == 32 @@ -4756,9 +4756,9 @@ def _mock_bot_info(*args, **kwargs): assert story_graph.story_steps[15].events[2].entities[0]['entity'] == 'fdresponse' domain = mongo_processor.load_domain(bot) assert isinstance(domain, Domain) - assert domain.slots.__len__() == 17 + assert domain.slots.__len__() == 18 assert domain.templates.keys().__len__() == 27 - assert domain.entities.__len__() == 16 + assert domain.entities.__len__() == 17 assert domain.form_names.__len__() == 2 assert domain.user_actions.__len__() == 40 assert domain.intents.__len__() == 29 @@ -4811,8 +4811,8 @@ def _mock_bot_info(*args, **kwargs): assert story_graph.story_steps[15].events[2].entities[0]['entity'] == 'fdresponse' domain = mongo_processor.load_domain(bot) assert isinstance(domain, Domain) - assert domain.slots.__len__() == 18 - assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 7 + assert domain.slots.__len__() == 19 + assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 8 assert len([slot for slot in domain.slots if slot.influence_conversation is False]) == 11 assert domain.intent_properties.__len__() == 32 assert len([intent for intent in domain.intent_properties.keys() if @@ -4820,7 +4820,7 @@ def _mock_bot_info(*args, **kwargs): assert len([intent for intent in domain.intent_properties.keys() if not domain.intent_properties.get(intent)['used_entities']]) == 5 assert domain.templates.keys().__len__() == 29 - assert domain.entities.__len__() == 17 + assert domain.entities.__len__() == 18 assert domain.form_names.__len__() == 2 assert domain.user_actions.__len__() == 48 assert domain.intents.__len__() == 32 @@ -4874,8 +4874,8 @@ def _mock_bot_info(*args, **kwargs): assert story_graph.story_steps[15].events[2].entities[0]['entity'] == 'fdresponse' domain = mongo_processor.load_domain(bot) assert isinstance(domain, Domain) - assert domain.slots.__len__() == 18 - assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 7 + assert domain.slots.__len__() == 19 + assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 8 assert len([slot for slot in domain.slots if slot.influence_conversation is False]) == 11 assert domain.intent_properties.__len__() == 33 assert len([intent for intent in domain.intent_properties.keys() if @@ -4883,7 +4883,7 @@ def _mock_bot_info(*args, **kwargs): assert len([intent for intent in domain.intent_properties.keys() if not domain.intent_properties.get(intent)['used_entities']]) == 6 assert domain.templates.keys().__len__() == 31 - assert domain.entities.__len__() == 17 + assert domain.entities.__len__() == 18 assert domain.form_names.__len__() == 2 assert domain.user_actions.__len__() == 50 assert domain.intents.__len__() == 33 @@ -4924,8 +4924,8 @@ def test_delete_nlu_only(self): assert story_graph.story_steps[15].events[2].entities[0]['entity'] == 'fdresponse' domain = mongo_processor.load_domain(bot) assert isinstance(domain, Domain) - assert domain.slots.__len__() == 18 - assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 7 + assert domain.slots.__len__() == 19 + assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 8 assert len([slot for slot in domain.slots if slot.influence_conversation is False]) == 11 assert domain.intent_properties.__len__() == 33 assert len([intent for intent in domain.intent_properties.keys() if @@ -4933,7 +4933,7 @@ def test_delete_nlu_only(self): assert len([intent for intent in domain.intent_properties.keys() if not domain.intent_properties.get(intent)['used_entities']]) == 6 assert domain.templates.keys().__len__() == 31 - assert domain.entities.__len__() == 17 + assert domain.entities.__len__() == 18 assert domain.form_names.__len__() == 2 assert domain.user_actions.__len__() == 50 assert domain.intents.__len__() == 33 @@ -4981,8 +4981,8 @@ def test_delete_stories_only(self): assert story_graph.story_steps.__len__() == 0 domain = mongo_processor.load_domain(bot) assert isinstance(domain, Domain) - assert domain.slots.__len__() == 18 - assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 7 + assert domain.slots.__len__() == 19 + assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 8 assert len([slot for slot in domain.slots if slot.influence_conversation is False]) == 11 assert domain.intent_properties.__len__() == 33 assert len([intent for intent in domain.intent_properties.keys() if @@ -4990,7 +4990,7 @@ def test_delete_stories_only(self): assert len([intent for intent in domain.intent_properties.keys() if not domain.intent_properties.get(intent)['used_entities']]) == 6 assert domain.templates.keys().__len__() == 31 - assert domain.entities.__len__() == 17 + assert domain.entities.__len__() == 18 assert domain.form_names.__len__() == 2 assert domain.user_actions.__len__() == 50 assert domain.intents.__len__() == 33 @@ -5026,8 +5026,8 @@ def test_delete_multiflow_stories_only(self): assert story_graph.story_steps.__len__() == 0 domain = mongo_processor.load_domain(bot) assert isinstance(domain, Domain) - assert domain.slots.__len__() == 18 - assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 7 + assert domain.slots.__len__() == 19 + assert len([slot for slot in domain.slots if slot.influence_conversation is True]) == 8 assert len([slot for slot in domain.slots if slot.influence_conversation is False]) == 11 assert domain.intent_properties.__len__() == 33 assert len([intent for intent in domain.intent_properties.keys() if @@ -5035,7 +5035,7 @@ def test_delete_multiflow_stories_only(self): assert len([intent for intent in domain.intent_properties.keys() if not domain.intent_properties.get(intent)['used_entities']]) == 6 assert domain.templates.keys().__len__() == 31 - assert domain.entities.__len__() == 17 + assert domain.entities.__len__() == 18 assert domain.form_names.__len__() == 2 assert domain.user_actions.__len__() == 50 assert domain.intents.__len__() == 33 @@ -5080,10 +5080,10 @@ def test_delete_config_and_actions_only(self): assert story_graph.story_steps.__len__() == 16 domain = mongo_processor.load_domain(bot) assert isinstance(domain, Domain) - assert domain.slots.__len__() == 18 + assert domain.slots.__len__() == 19 assert domain.intent_properties.__len__() == 33 assert domain.templates.keys().__len__() == 31 - assert domain.entities.__len__() == 17 + assert domain.entities.__len__() == 18 assert domain.form_names.__len__() == 2 assert domain.user_actions.__len__() == 43 assert domain.intents.__len__() == 33 @@ -5156,10 +5156,10 @@ async def test_save_rules_and_domain_only(self, get_training_data): assert len(rules) == 3 domain = mongo_processor.load_domain(bot) assert isinstance(domain, Domain) - assert domain.slots.__len__() == 18 + assert domain.slots.__len__() == 19 assert domain.intent_properties.__len__() == 32 assert domain.templates.keys().__len__() == 27 - assert domain.entities.__len__() == 17 + assert domain.entities.__len__() == 18 assert domain.form_names.__len__() == 2 assert domain.user_actions.__len__() == 46 assert domain.intents.__len__() == 32 @@ -6841,7 +6841,7 @@ def test_get_slot(self): bot = 'test' processor = MongoProcessor() slots = list(processor.get_existing_slots(bot)) - assert len(slots) == 18 + assert len(slots) == 19 assert slots == [ {'name': 'bot', 'type': 'any', 'initial_value': 'test', 'auto_fill': False, 'influence_conversation': False, '_has_been_set': False}, @@ -6859,6 +6859,8 @@ def test_get_slot(self): '_has_been_set': False}, {'name': 'doc_url', 'type': 'text', 'auto_fill': True, 'influence_conversation': True, '_has_been_set': False}, + {'name': 'flow_reply', 'type': 'text', 'auto_fill': True, 'influence_conversation': True, + '_has_been_set': False}, {'name': 'category', 'type': 'unfeaturized', 'auto_fill': True, 'influence_conversation': False, '_has_been_set': False}, {'name': 'file', 'type': 'unfeaturized', 'auto_fill': True, 'influence_conversation': False,