diff --git a/kairon/chat/handlers/channels/clients/whatsapp/cloud.py b/kairon/chat/handlers/channels/clients/whatsapp/cloud.py index 672d2f5b3..a1d2c9639 100644 --- a/kairon/chat/handlers/channels/clients/whatsapp/cloud.py +++ b/kairon/chat/handlers/channels/clients/whatsapp/cloud.py @@ -46,6 +46,7 @@ def __init__(self, access_token, **kwargs): self.api_version = kwargs.get('api_version', DEFAULT_API_VERSION) self.app = 'https://graph.facebook.com/v{api_version}'.format(api_version=self.api_version) self.app_secret = kwargs.get('app_secret') + self.metadata = kwargs.get("metadata") @property def client_type(self): diff --git a/kairon/chat/handlers/channels/whatsapp.py b/kairon/chat/handlers/channels/whatsapp.py index 4c72d6726..cf8cac718 100644 --- a/kairon/chat/handlers/channels/whatsapp.py +++ b/kairon/chat/handlers/channels/whatsapp.py @@ -158,6 +158,7 @@ async def _handle_user_message( ) -> None: """Pass on the text to the dialogue engine for processing.""" out_channel = WhatsappBot(self.client) + self.client.metadata = metadata await out_channel.mark_as_read(metadata["id"]) user_msg = UserMessage( text, out_channel, sender_id, input_channel=self.name(), metadata=metadata @@ -238,7 +239,24 @@ async def send_custom_json( from kairon.chat.converters.channels.response_factory import ConverterFactory converter_instance = ConverterFactory.getConcreteInstance(messagetype, ChannelTypes.WHATSAPP.value) response = await converter_instance.messageConverter(message) - self.whatsapp_client.send(response, recipient_id, messaging_type) + resp = self.whatsapp_client.send(response, recipient_id, messaging_type) + + if resp.get("error"): + bot = kwargs.get("assistant_id") + message_id = self.whatsapp_client.metadata.get("id") + user = self.whatsapp_client.metadata.get("display_phone_number") + if not bot: + logger.error("Missing assistant_id in kwargs for failed message logging") + return + logger.error(f"WhatsApp message failed: {resp.get('error')}") + try: + ChatDataProcessor.save_whatsapp_failed_messages( + resp, bot, recipient_id, ChannelTypes.WHATSAPP.value, + json_message=json_message, message_id=message_id, user=user, + metadata=self.whatsapp_client.metadata + ) + except Exception as e: + logger.error(f"Failed to log WhatsApp error: {str(e)}") else: self.send(recipient_id, {"preview_url": True, "body": str(json_message)}) diff --git a/kairon/shared/chat/processor.py b/kairon/shared/chat/processor.py index f23f6a361..43d136e50 100644 --- a/kairon/shared/chat/processor.py +++ b/kairon/shared/chat/processor.py @@ -167,6 +167,41 @@ def save_whatsapp_audit_log(status_data: Dict, bot: Text, user: Text, recipient: campaign_id=campaign_id ).save() + @staticmethod + def save_whatsapp_failed_messages(resp: Dict, bot: Text, recipient: Text, channel_type: Text, **kwargs): + """ + Logs failed WhatsApp message responses for debugging and tracking purposes. + + Args: + resp (Dict): The response dictionary containing error details. + bot (Text): The bot identifier. + recipient (Text): The recipient identifier. + channel_type (Text): The type of channel (e.g., WhatsApp). + + Returns: + None + """ + error = resp.get("error", {}) + message_id = kwargs.get("message_id") + user = kwargs.get("user") + json_message = kwargs.get('json_message') + metadata = kwargs.get("metadata") + failure_reason = error.get("error_data", {}).get("details") + logger.debug(f"WhatsApp message failed to send: {error}") + + ChannelLogs( + type=channel_type, + status="failed", + data=resp, + failure_reason=failure_reason, + message_id=message_id, + user=user, + bot=bot, + recipient=recipient, + json_message=json_message, + metadata=metadata + ).save() + @staticmethod def get_instagram_static_comment(bot: str) -> str: channel = ChatDataProcessor.get_channel_config(bot=bot, connector_type="instagram", mask_characters=False) diff --git a/tests/unit_test/chat/chat_test.py b/tests/unit_test/chat/chat_test.py index 2e94b39d8..36f5b1000 100644 --- a/tests/unit_test/chat/chat_test.py +++ b/tests/unit_test/chat/chat_test.py @@ -198,6 +198,180 @@ def __mock_get_bot(*args, **kwargs): "client_secret": "a23456789sfdghhtyutryuivcbn", "is_primary": False}}, "test", "test" ) + def test_save_whatsapp_failed_messages(self): + from kairon.shared.chat.data_objects import ChannelLogs + json_message = { + 'data': [ + { + 'type': 'button', + 'value': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + 'id': 1, + 'children': [ + { + 'text': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + } + ] + } + ], + 'type': 'button' + } + + resp = { + 'error': { + 'message': '(#131009) Parameter value is not valid', 'type': 'OAuthException', + 'code': 131009, + 'error_data': { + 'messaging_product': 'whatsapp', + 'details': 'Button title length invalid. Min length: 1, Max length: 20' + }, + 'fbtrace_id': 'ALfhCiNWtld8enTUJyg0FFo' + } + } + bot = "5e564fbcdcf0d5fad89e3abd" + recipient = "919876543210" + channel_type = "whatsapp" + message_id = "wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA" + user = "91865712345" + ChatDataProcessor.save_whatsapp_failed_messages(resp, bot, recipient, channel_type, json_message=json_message, + message_id=message_id, user=user) + log = ( + ChannelLogs.objects( + bot=bot, + message_id="wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA", + ) + .get() + .to_mongo() + .to_dict() + ) + print(log) + assert log['type'] == 'whatsapp' + assert log['data'] == resp + assert log['status'] == 'failed' + assert log['message_id'] == 'wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA' + assert log['failure_reason'] == 'Button title length invalid. Min length: 1, Max length: 20' + assert log['recipient'] == '919876543210' + assert log['type'] == 'whatsapp' + assert log['user'] == '91865712345' + assert log['json_message'] == { + 'data': [ + { + 'type': 'button', + 'value': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + 'id': 1, + 'children': [ + { + 'text': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + } + ] + } + ], + 'type': 'button' + } + + @pytest.mark.asyncio + @mock.patch("kairon.chat.handlers.channels.clients.whatsapp.dialog360.BSP360Dialog.send", autospec=True) + async def test_save_whatsapp_failed_messages_through_send_custom_json(self, mock_bsp): + from kairon.shared.chat.data_objects import ChannelLogs + from kairon.chat.handlers.channels.whatsapp import WhatsappBot + from kairon.chat.handlers.channels.clients.whatsapp.dialog360 import BSP360Dialog + + json_message = { + 'data': [ + { + 'type': 'button', + 'value': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + 'id': 1, + 'children': [ + { + 'text': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + } + ] + } + ], + 'type': 'button' + } + + resp = { + 'error': { + 'message': '(#131009) Parameter value is not valid', 'type': 'OAuthException', + 'code': 131009, + 'error_data': { + 'messaging_product': 'whatsapp', + 'details': 'Button title length invalid. Min length: 1, Max length: 20' + }, + 'fbtrace_id': 'ALfhCiNWtld8enTUJyg0FFo' + } + } + mock_bsp.return_value = resp + bot = "5e564fbcdcf0d5fad89e3abcd" + recipient = "919876543210" + user = "91865712345" + channel_type = "whatsapp" + metadata = { + 'from': '919876543210', + 'id': 'wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA', + 'timestamp': '1732622052', + 'text': {'body': '/btn'}, + 'type': 'text', + 'is_integration_user': True, + 'bot': bot, + 'account': 8, + 'channel_type': 'whatsapp', + 'bsp_type': '360dialog', + 'tabname': 'default', + 'display_phone_number': '91865712345', + 'phone_number_id': '142427035629239' + } + client = BSP360Dialog("asajjdkjskdkdjfk", metadata=metadata) + await WhatsappBot(client).send_custom_json(recipient, json_message, assistant_id=bot) + log = ( + ChannelLogs.objects( + bot=bot, + message_id="wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA", + ) + .get() + .to_mongo() + .to_dict() + ) + assert log['type'] == 'whatsapp' + assert log['data'] == resp + assert log['status'] == 'failed' + assert log['message_id'] == 'wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA' + assert log['failure_reason'] == 'Button title length invalid. Min length: 1, Max length: 20' + assert log['recipient'] == '919876543210' + assert log['type'] == 'whatsapp' + assert log['user'] == '91865712345' + assert log['json_message'] == { + 'data': [ + { + 'type': 'button', + 'value': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + 'id': 1, + 'children': [ + { + 'text': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + } + ] + } + ], + 'type': 'button' + } + assert log['metadata'] == { + 'from': recipient, + 'id': 'wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA', + 'timestamp': '1732622052', + 'text': {'body': '/btn'}, + 'type': 'text', + 'is_integration_user': True, + 'bot': bot, + 'account': 8, + 'channel_type': 'whatsapp', + 'bsp_type': '360dialog', + 'tabname': 'default', + 'display_phone_number': user, + 'phone_number_id': '142427035629239' + } + def test_list_channels(self): channels = list(ChatDataProcessor.list_channel_config("test")) assert channels.__len__() == 3