diff --git a/kairon/chat/converters/channels/messenger.py b/kairon/chat/converters/channels/messenger.py index 73cdd9b87..f0f77234f 100644 --- a/kairon/chat/converters/channels/messenger.py +++ b/kairon/chat/converters/channels/messenger.py @@ -1,3 +1,5 @@ +import json + from kairon.chat.converters.channels.responseconverter import ElementTransformerOps from kairon.shared.constants import ElementTypes @@ -34,6 +36,25 @@ def link_transformer(self, message): except Exception as ex: raise Exception(f" Error in MessengerResponseConverter::link_transformer {str(ex)}") + def paragraph_transformer(self, message): + try: + message_template = ElementTransformerOps.getChannelConfig(self.channel, self.message_type) + paragraph_template = json.loads(message_template) + jsoniterator = ElementTransformerOps.json_generator(message) + final_text = "" + for item in jsoniterator: + if item.get("type") == "paragraph": + children = ElementTransformerOps.json_generator(item.get("children", [])) + for child in children: + text = child.get("text", "") + final_text += text + final_text += "\n" + + paragraph_template["text"] = final_text + return paragraph_template + except Exception as ex: + raise Exception(f"Error in MessengerResponseConverter::paragraph_transformer {str(ex)}") + def button_transformer(self, message): try: button_json_temp = {} @@ -89,5 +110,7 @@ async def messageConverter(self, message): return self.button_transformer(message) elif self.message_type == ElementTypes.QUICK_REPLY.value: return self.quick_reply_transformer(message) + elif self.message_type == ElementTypes.FORMAT_TEXT.value: + return self.paragraph_transformer(message) except Exception as ex: raise Exception(f"Error in MessengerResponseConverter::messageConverter {str(ex)}") diff --git a/kairon/chat/converters/channels/telegram.py b/kairon/chat/converters/channels/telegram.py index 9b5b83d72..735bda41d 100644 --- a/kairon/chat/converters/channels/telegram.py +++ b/kairon/chat/converters/channels/telegram.py @@ -1,3 +1,5 @@ +import json + from kairon.chat.converters.channels.responseconverter import ElementTransformerOps from kairon.shared.constants import ElementTypes @@ -34,6 +36,34 @@ def link_transformer(self, message): except Exception as ex: raise Exception(f" Error in TelegramResponseConverter::link_transformer {str(ex)}") + def paragraph_transformer(self, message): + try: + message_template = ElementTransformerOps.getChannelConfig(self.channel, self.message_type) + paragraph_template = json.loads(message_template) + jsoniterator = ElementTransformerOps.json_generator(message) + final_text = "" + for item in jsoniterator: + if item.get("type") == "paragraph": + children = ElementTransformerOps.json_generator(item.get("children", [])) + for child in children: + text = child.get("text", "") + leading_spaces = len(text) - len(text.lstrip()) + trailing_spaces = len(text) - len(text.rstrip()) + text = text.strip() + if child.get("bold"): + text = f"{text}" + if child.get("italic"): + text = f"{text}" + if child.get("strikethrough"): + text = f"{text}" + final_text += f"{' ' * leading_spaces}{text}{' ' * trailing_spaces}" + final_text += "\n" + + paragraph_template["text"] = final_text + return paragraph_template + except Exception as ex: + raise Exception(f"Error in TelegramResponseConverter::paragraph_transformer {str(ex)}") + def button_transformer(self, message): try: jsoniterator = ElementTransformerOps.json_generator(message) @@ -65,5 +95,7 @@ async def messageConverter(self, message): return super().video_transformer(message) elif self.message_type == ElementTypes.BUTTON.value: return self.button_transformer(message) + elif self.message_type == ElementTypes.FORMAT_TEXT.value: + return self.paragraph_transformer(message) except Exception as ex: raise Exception(f"Error in TelegramResponseConverter::messageConverter {str(ex)}") diff --git a/kairon/chat/converters/channels/whatsapp.py b/kairon/chat/converters/channels/whatsapp.py index 8448fee64..1cd9135b8 100644 --- a/kairon/chat/converters/channels/whatsapp.py +++ b/kairon/chat/converters/channels/whatsapp.py @@ -1,6 +1,6 @@ + from kairon.chat.converters.channels.responseconverter import ElementTransformerOps import ujson as json - from kairon.shared.constants import ElementTypes @@ -34,6 +34,34 @@ def link_transformer(self, message): except Exception as ex: raise Exception(f"Error in WhatsappResponseConverter::link_transformer {str(ex)}") + def paragraph_transformer(self, message): + try: + message_template = ElementTransformerOps.getChannelConfig(self.channel, self.message_type) + paragraph_template = json.loads(message_template) + jsoniterator = ElementTransformerOps.json_generator(message) + final_text = "" + for item in jsoniterator: + if item.get("type") == "paragraph": + children = ElementTransformerOps.json_generator(item.get("children", [])) + for child in children: + text = child.get("text", "") + leading_spaces = len(text) - len(text.lstrip()) + trailing_spaces = len(text) - len(text.rstrip()) + text = text.strip() + if child.get("bold"): + text = f"*{text}*" + if child.get("italic"): + text = f"_{text}_" + if child.get("strikethrough"): + text = f"~{text}~" + final_text += f"{' ' * leading_spaces}{text}{' ' * trailing_spaces}" + final_text += "\n" + + paragraph_template["body"] = final_text + return paragraph_template + except Exception as ex: + raise Exception(f"Error in WhatsappResponseConverter::paragraph_transformer {str(ex)}") + def button_transformer(self, message): try: message_template = ElementTransformerOps.getChannelConfig(self.channel, self.message_type) @@ -121,5 +149,7 @@ async def messageConverter(self, message): return self.button_transformer(message) elif self.message_type == ElementTypes.DROPDOWN.value: return self.dropdown_transformer(message) + elif self.message_type == ElementTypes.FORMAT_TEXT.value: + return self.paragraph_transformer(message) except Exception as ex: raise Exception(f"Error in WhatsappResponseConverter::messageConverter {str(ex)}") diff --git a/kairon/chat/handlers/channels/telegram.py b/kairon/chat/handlers/channels/telegram.py index bace014d2..e48f2cd8c 100644 --- a/kairon/chat/handlers/channels/telegram.py +++ b/kairon/chat/handlers/channels/telegram.py @@ -156,7 +156,7 @@ async def send_custom_json( del response["photo"] api_call = getattr(self, send_functions[("photo",)]) api_call(recipient_id, *response_list, **response) - elif ops_type in ["link", "video"]: + elif ops_type in ["link", "video", "formatText"]: response_list.append(response.get("text")) del response["text"] api_call = getattr(self, send_functions[("text",)]) diff --git a/kairon/chat/handlers/channels/whatsapp.py b/kairon/chat/handlers/channels/whatsapp.py index cf8cac718..2cd24f135 100644 --- a/kairon/chat/handlers/channels/whatsapp.py +++ b/kairon/chat/handlers/channels/whatsapp.py @@ -233,7 +233,7 @@ async def send_custom_json( message = json_message.get("data") messagetype = json_message.get("type") content_type = {"link": "text", "video": "video", "image": "image", "button": "interactive", - "dropdown": "interactive", "audio": "audio"} + "dropdown": "interactive", "audio": "audio", "formatText": "text"} if messagetype is not None and messagetype in type_list: messaging_type = content_type.get(messagetype) from kairon.chat.converters.channels.response_factory import ConverterFactory diff --git a/kairon/shared/constants.py b/kairon/shared/constants.py index 36e23ee55..c9e53f37b 100644 --- a/kairon/shared/constants.py +++ b/kairon/shared/constants.py @@ -126,6 +126,7 @@ class ElementTypes(str, Enum): BUTTON = "button" DROPDOWN = "dropdown" QUICK_REPLY = "quick_reply" + FORMAT_TEXT = "formatText" class WhatsappBSPTypes(str, Enum): diff --git a/metadata/message_template.yml b/metadata/message_template.yml index 5533b115e..aa99e6533 100644 --- a/metadata/message_template.yml +++ b/metadata/message_template.yml @@ -1,4 +1,4 @@ -type_list: ["image","link","video","button","quick_reply","dropdown","audio"] +type_list: ["image","link","video","button","quick_reply","dropdown","audio", "formatText"] slack: image: '{ "blocks": [ @@ -88,6 +88,7 @@ messenger: video: '{"text":""}' button: '{"text":""}' body_message: "Please select from quick buttons:" + formatText: '{"text":""}' whatsapp: image: '{ @@ -112,6 +113,10 @@ whatsapp: "action":"" }' body_message: "Please select from quick buttons:" + formatText: '{ + "preview_url": true, + "body":"" + }' dropdown: '{ "type": "list", @@ -128,6 +133,13 @@ telegram: "reply_to_message_id":0}' video: '{"text":""}' body_message: 'Please select from quick buttons:' + formatText: '{ + "text":"", + "parse_mode":"HTML", + "disable_web_page_preview":false, + "disable_notification":false, + "reply_to_message_id":0 + }' msteams: body_message: "Please select from quick buttons:" diff --git a/tests/unit_test/utility_test.py b/tests/unit_test/utility_test.py index e9ca44ee1..3754bcff8 100644 --- a/tests/unit_test/utility_test.py +++ b/tests/unit_test/utility_test.py @@ -8,6 +8,9 @@ from io import BytesIO from unittest.mock import patch, MagicMock from urllib.parse import urlencode + +from kairon.chat.converters.channels.messenger import MessengerResponseConverter +from kairon.chat.converters.channels.whatsapp import WhatsappResponseConverter from kairon.shared.utils import Utility, MailUtility Utility.load_system_metadata() @@ -32,11 +35,11 @@ from kairon.chat.converters.channels.telegram import TelegramResponseConverter from kairon.exceptions import AppException from kairon.shared.augmentation.utils import AugmentationUtils -from kairon.shared.constants import GPT3ResourceTypes, LLMResourceProvider +from kairon.shared.constants import ElementTypes from kairon.shared.data.audit.data_objects import AuditLogData from kairon.shared.data.audit.processor import AuditDataProcessor -from kairon.shared.data.constant import DEFAULT_SYSTEM_PROMPT, STORY_EVENT -from kairon.shared.data.data_objects import EventConfig, Slots, LLMSettings, DemoRequestLogs +from kairon.shared.data.constant import STORY_EVENT +from kairon.shared.data.data_objects import EventConfig, Slots, DemoRequestLogs from kairon.shared.data.processor import MongoProcessor from kairon.shared.data.utils import DataUtility from kairon.shared.models import TemplateType @@ -3333,3 +3336,202 @@ def test_comma_sep_string_to_list(self): # Test input with spaces assert Utility.string_to_list("apple, banana, orange") == ["apple", "banana", "orange"] + + + def test_whatsapp_paragraph_transformer_basic(self): + whatsapp_response_converter = WhatsappResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, + channel_type="whatsapp") + message = [ + { + "type": "paragraph", + "children": [ + {"text": "This is a test paragraph."} + ] + } + ] + expected_output = {"body": "This is a test paragraph.\n"} + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', + return_value='{"body": ""}'): + response = whatsapp_response_converter.paragraph_transformer(message) + assert response == expected_output + + def test_whatsapp_paragraph_transformer_with_formatting(self): + whatsapp_response_converter = WhatsappResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, + channel_type="whatsapp") + message = [ + { + "type": "paragraph", + "children": [ + {"text": "Bold text", "bold": True}, + {"text": " and normal text."} + ] + } + ] + expected_output = {"body": "*Bold text* and normal text.\n"} + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', + return_value='{"body": ""}'): + response = whatsapp_response_converter.paragraph_transformer(message) + assert response == expected_output + + def test_whatsapp_paragraph_transformer_with_spaces(self): + whatsapp_response_converter = WhatsappResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, + channel_type="whatsapp") + message = [ + { + "type": "paragraph", + "children": [ + {"text": " Text with leading and trailing spaces "} + ] + } + ] + expected_output = {"body": " Text with leading and trailing spaces \n"} + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', + return_value='{"body": ""}'): + response = whatsapp_response_converter.paragraph_transformer(message) + assert response == expected_output + + def test_whatsapp_paragraph_transformer_empty_message(self): + whatsapp_response_converter = WhatsappResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, + channel_type="whatsapp") + message = [] + expected_output = {"body": ""} + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', + return_value='{"body": ""}'): + response = whatsapp_response_converter.paragraph_transformer(message) + assert response == expected_output + + def test_whatsapp_paragraph_transformer_exception_handling(self): + whatsapp_response_converter = WhatsappResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, + channel_type="whatsapp") + message = None + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', + side_effect=Exception("Test Exception")): + with pytest.raises(Exception) as excinfo: + whatsapp_response_converter.paragraph_transformer(message) + assert "Error in WhatsappResponseConverter::paragraph_transformer" in str(excinfo.value) + + + def test_telegram_paragraph_transformer_basic(self): + telegram_response_converter = TelegramResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, channel_type="telegram") + message = [ + { + "type": "paragraph", + "children": [ + {"text": "This is a test paragraph."} + ] + } + ] + expected_output = {"text": "This is a test paragraph.\n"} + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', return_value='{"text": ""}'): + response = telegram_response_converter.paragraph_transformer(message) + assert response == expected_output + + def test_telegram_paragraph_transformer_with_formatting(self): + telegram_response_converter = TelegramResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, channel_type="telegram") + message = [ + { + "type": "paragraph", + "children": [ + {"text": "Bold text", "bold": True}, + {"text": " and normal text."} + ] + } + ] + expected_output = {"text": "Bold text and normal text.\n"} + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', return_value='{"text": ""}'): + response = telegram_response_converter.paragraph_transformer(message) + assert response == expected_output + + def test_telegram_paragraph_transformer_with_spaces(self): + telegram_response_converter = TelegramResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, channel_type="telegram") + message = [ + { + "type": "paragraph", + "children": [ + {"text": " Text with leading and trailing spaces "} + ] + } + ] + expected_output = {"text": " Text with leading and trailing spaces \n"} + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', return_value='{"text": ""}'): + response = telegram_response_converter.paragraph_transformer(message) + assert response == expected_output + + def test_telegram_paragraph_transformer_empty_message(self): + telegram_response_converter = TelegramResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, channel_type="telegram") + message = [] + expected_output = {"text": ""} + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', return_value='{"text": ""}'): + response = telegram_response_converter.paragraph_transformer(message) + assert response == expected_output + + def test_telegram_paragraph_transformer_exception_handling(self): + telegram_response_converter = TelegramResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, channel_type="telegram") + message = None + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', side_effect=Exception("Test Exception")): + with pytest.raises(Exception) as excinfo: + telegram_response_converter.paragraph_transformer(message) + assert "Error in TelegramResponseConverter::paragraph_transformer" in str(excinfo.value) + + + def test_messenger_client_paragraph_transformer_basic(self): + messenger_response_converter = MessengerResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, channel_type="messenger") + message = [ + { + "type": "paragraph", + "children": [ + {"text": "This is a test paragraph."} + ] + } + ] + expected_output = {"text": "This is a test paragraph.\n"} + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', return_value='{"text": ""}'): + response = messenger_response_converter.paragraph_transformer(message) + assert response == expected_output + + def test_messenger_client_paragraph_transformer_with_formatting(self): + messenger_response_converter = MessengerResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, channel_type="messenger") + message = [ + { + "type": "paragraph", + "children": [ + {"text": "Bold text", "bold": True}, + {"text": " and normal text."} + ] + } + ] + expected_output = {"text": "Bold text and normal text.\n"} + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', return_value='{"text": ""}'): + response = messenger_response_converter.paragraph_transformer(message) + assert response == expected_output + + def test_messenger_client_paragraph_transformer_with_spaces(self): + messenger_response_converter = MessengerResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, channel_type="messenger") + message = [ + { + "type": "paragraph", + "children": [ + {"text": " Text with leading and trailing spaces "} + ] + } + ] + expected_output = {"text": " Text with leading and trailing spaces \n"} + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', return_value='{"text": ""}'): + response = messenger_response_converter.paragraph_transformer(message) + assert response == expected_output + + def test_messenger_client_paragraph_transformer_empty_message(self): + messenger_response_converter = MessengerResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, channel_type="messenger") + message = [] + expected_output = {"text": ""} + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', return_value='{"text": ""}'): + response = messenger_response_converter.paragraph_transformer(message) + assert response == expected_output + + def test_messenger_client_paragraph_transformer_exception_handling(self): + messenger_response_converter = MessengerResponseConverter(message_type=ElementTypes.FORMAT_TEXT.value, channel_type="messenger") + message = None + with patch('kairon.chat.converters.channels.responseconverter.ElementTransformerOps.getChannelConfig', side_effect=Exception("Test Exception")): + with pytest.raises(Exception) as excinfo: + messenger_response_converter.paragraph_transformer(message) + assert "Error in MessengerResponseConverter::paragraph_transformer" in str(excinfo.value) \ No newline at end of file