From b66d134857563857681a8082edc536a0b1a35715 Mon Sep 17 00:00:00 2001 From: Mahesh Date: Tue, 4 Jun 2024 15:53:07 +0530 Subject: [PATCH 1/2] 1. Added code for the Flow Action 2. added APIs along with unit and integration test cases for the same. --- kairon/actions/definitions/factory.py | 4 +- kairon/actions/definitions/flow.py | 91 +++ kairon/api/app/routers/bot/action.py | 38 +- kairon/api/models.py | 59 +- kairon/shared/actions/data_objects.py | 40 ++ kairon/shared/actions/models.py | 11 + kairon/shared/actions/utils.py | 51 ++ kairon/shared/admin/constants.py | 1 + kairon/shared/data/processor.py | 74 +++ system.yaml | 5 + tests/integration_test/action_service_test.py | 474 +++++++++++++- tests/integration_test/services_test.py | 590 +++++++++++++++++- tests/testing_data/system.yaml | 5 + .../data_processor/data_processor_test.py | 396 +++++++++++- 14 files changed, 1830 insertions(+), 9 deletions(-) create mode 100644 kairon/actions/definitions/flow.py diff --git a/kairon/actions/definitions/factory.py b/kairon/actions/definitions/factory.py index 8cee637f4..959cabc82 100644 --- a/kairon/actions/definitions/factory.py +++ b/kairon/actions/definitions/factory.py @@ -2,6 +2,7 @@ from kairon.actions.definitions.bot_response import ActionKaironBotResponse from kairon.actions.definitions.email import ActionEmail +from kairon.actions.definitions.flow import ActionFlow from kairon.actions.definitions.form_validation import ActionFormValidation from kairon.actions.definitions.google import ActionGoogleSearch from kairon.actions.definitions.http import ActionHTTP @@ -41,7 +42,8 @@ class ActionFactory: ActionType.pyscript_action.value: ActionPyscript, ActionType.database_action.value: ActionDatabase, ActionType.web_search_action.value: ActionWebSearch, - ActionType.live_agent_action.value: ActionLiveAgent + ActionType.live_agent_action.value: ActionLiveAgent, + ActionType.flow_action.value: ActionFlow } @staticmethod diff --git a/kairon/actions/definitions/flow.py b/kairon/actions/definitions/flow.py new file mode 100644 index 000000000..039d91ac3 --- /dev/null +++ b/kairon/actions/definitions/flow.py @@ -0,0 +1,91 @@ +from typing import Text, Dict, Any + +from loguru import logger +from mongoengine import DoesNotExist +from rasa_sdk import Tracker +from rasa_sdk.executor import CollectingDispatcher + +from kairon.actions.definitions.base import ActionsBase +from kairon.shared.actions.data_objects import FlowActionConfig, ActionServerLogs +from kairon.shared.actions.exception import ActionFailure +from kairon.shared.actions.models import ActionType +from kairon.shared.actions.utils import ActionUtility +from kairon.shared.constants import KaironSystemSlots + + +class ActionFlow(ActionsBase): + + def __init__(self, bot: Text, name: Text): + """ + Initialize Flow action. + @param bot: bot id + @param name: action name + """ + self.bot = bot + self.name = name + + def retrieve_config(self): + """ + Fetch Flow action configuration parameters from the database + :return: FlowActionConfig containing configuration for the action as a dict. + """ + try: + http_config_dict = FlowActionConfig.objects().get(bot=self.bot, + name=self.name, + status=True).to_mongo().to_dict() + logger.debug("flow_action_config: " + str(http_config_dict)) + return http_config_dict + except DoesNotExist as e: + logger.exception(e) + raise ActionFailure("No Flow action found for given action and bot") + + async def execute(self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any]): + """ + Retrieves action config and executes it. + Information regarding the execution is logged in ActionServerLogs. + @param dispatcher: Client to send messages back to the user. + @param tracker: Tracker object to retrieve slots, events, messages and other contextual information. + @param domain: Bot domain + :return: Dict containing slot name as keys and their values. + """ + bot_response = None + http_response = None + exception = None + status = "SUCCESS" + http_url = None + headers = {} + body = {} + dispatch_response = True + try: + flow_action_config = self.retrieve_config() + bot_response = flow_action_config['response'] + dispatch_response = flow_action_config['dispatch_response'] + body, http_url, headers = ActionUtility.prepare_flow_body(flow_action_config, tracker) + http_response, status_code, time_elapsed = await ActionUtility.execute_request_async( + headers=headers, http_url=http_url, request_method="POST", request_body=body + ) + logger.info("response: " + str(http_response)) + except Exception as e: + exception = str(e) + logger.exception(e) + status = "FAILURE" + bot_response = "I have failed to process your request" + finally: + if dispatch_response: + dispatcher.utter_message(bot_response) + ActionServerLogs( + type=ActionType.flow_action.value, + intent=tracker.get_intent_of_latest_message(skip_fallback_intent=False), + action=self.name, + sender=tracker.sender_id, + bot=tracker.get_slot("bot"), + url=http_url, + headers=headers, + request_params=body, + exception=exception, + api_response=str(http_response) if http_response else None, + bot_response=bot_response, + status=status, + user_msg=tracker.latest_message.get('text') + ).save() + return {KaironSystemSlots.kairon_action_response.value: bot_response} diff --git a/kairon/api/app/routers/bot/action.py b/kairon/api/app/routers/bot/action.py index d0aa2e63d..dbbe15e66 100644 --- a/kairon/api/app/routers/bot/action.py +++ b/kairon/api/app/routers/bot/action.py @@ -7,7 +7,7 @@ HttpActionConfigRequest, SlotSetActionRequest, EmailActionRequest, GoogleSearchActionRequest, JiraActionRequest, ZendeskActionRequest, PipedriveActionRequest, HubspotFormsActionRequest, TwoStageFallbackConfigRequest, RazorpayActionRequest, PromptActionConfigRequest, DatabaseActionRequest, PyscriptActionRequest, - WebSearchActionRequest, LiveAgentActionRequest + WebSearchActionRequest, LiveAgentActionRequest, FlowActionRequest ) from kairon.shared.constants import TESTER_ACCESS, DESIGNER_ACCESS from kairon.shared.models import User @@ -66,6 +66,42 @@ async def update_http_action( return Response(data=response, message=message) +@router.post("/flow", response_model=Response) +async def add_flow_action( + request_data: FlowActionRequest, + current_user: User = Security(Authentication.get_current_user_and_bot, scopes=DESIGNER_ACCESS) +): + """ + Stores the flow action config + """ + flow_config_id = mongo_processor.add_flow_action(request_data.dict(), current_user.get_user(), + current_user.get_bot()) + return Response(data={"_id": flow_config_id}, message="Action added!") + + +@router.put("/flow", response_model=Response) +async def update_flow_action( + request_data: FlowActionRequest, + current_user: User = Security(Authentication.get_current_user_and_bot, scopes=DESIGNER_ACCESS) +): + """ + Updates the flow action config + """ + action_id = mongo_processor.update_flow_action(request_data=request_data.dict(), user=current_user.get_user(), + bot=current_user.get_bot()) + return Response(data={"_id": action_id}, message="Action updated!") + + +@router.get("/flow", response_model=Response) +async def list_flow_actions( + current_user: User = Security(Authentication.get_current_user_and_bot, scopes=TESTER_ACCESS)): + """ + Returns list of flow actions for bot. + """ + actions = list(mongo_processor.list_flow_actions(bot=current_user.get_bot())) + return Response(data=actions) + + @router.post("/pyscript", response_model=Response) async def add_pyscript_action( request_data: PyscriptActionRequest, diff --git a/kairon/api/models.py b/kairon/api/models.py index 9f24fb129..4e141ebbc 100644 --- a/kairon/api/models.py +++ b/kairon/api/models.py @@ -22,7 +22,8 @@ EvaluationType, DispatchType, DbQueryValueType, - DbActionOperationType, UserMessageType + DbActionOperationType, UserMessageType, + FlowModes, FlowActionTypes ) from ..shared.constants import SLOT_SET_TYPE, FORM_SLOT_SET_TYPE @@ -897,6 +898,62 @@ def validate_top_n(cls, v, values, **kwargs): return v +class FlowActionRequest(BaseModel): + name: constr(to_lower=True, strip_whitespace=True) + flow_id: CustomActionParameter + header: str = None + body: str + footer: str = None + mode: FlowModes = FlowModes.published.value + flow_action: FlowActionTypes = FlowActionTypes.navigate.value + flow_token: str + recipient_phone: CustomActionParameter + initial_screen: str + flow_cta: str + dispatch_response: bool = True + response: str = None + + @validator("name") + def validate_name(cls, v, values, **kwargs): + from kairon.shared.utils import Utility + + if Utility.check_empty_string(v): + raise ValueError("name is required") + return v + + @validator("body") + def validate_body(cls, v, values, **kwargs): + from kairon.shared.utils import Utility + + if Utility.check_empty_string(v): + raise ValueError("body is required") + return v + + @validator("initial_screen") + def validate_initial_screen(cls, v, values, **kwargs): + from kairon.shared.utils import Utility + + if Utility.check_empty_string(v): + raise ValueError("initial_screen is required") + return v + + @validator("flow_cta") + def validate_flow_cta(cls, v, values, **kwargs): + from kairon.shared.utils import Utility + + if Utility.check_empty_string(v): + raise ValueError("flow_cta is required") + return v + + @validator("flow_token") + def validate_flow_token(cls, v, values, **kwargs): + from kairon.shared.utils import Utility + + if Utility.check_empty_string(v): + raise ValueError("flow_token is required") + return v + + class EmailActionRequest(BaseModel): action_name: constr(to_lower=True, strip_whitespace=True) smtp_url: str diff --git a/kairon/shared/actions/data_objects.py b/kairon/shared/actions/data_objects.py index dc86e79e3..d5b61d7c7 100644 --- a/kairon/shared/actions/data_objects.py +++ b/kairon/shared/actions/data_objects.py @@ -27,6 +27,7 @@ DispatchType, DbQueryValueType, DbActionOperationType, UserMessageType, + FlowModes, FlowActionTypes ) from kairon.shared.constants import SLOT_SET_TYPE, FORM_SLOT_SET_TYPE from kairon.shared.data.audit.data_objects import Auditlog @@ -448,6 +449,45 @@ def clean(self): self.custom_text.key = "custom_text" +@auditlogger.log +@push_notification.apply +class FlowActionConfig(Auditlog): + name = StringField(required=True) + flow_id = EmbeddedDocumentField(CustomActionRequestParameters) + header = StringField(default=None) + body = StringField(required=True) + footer = StringField(default=None) + mode = StringField(default=FlowModes.published.value, + choices=[p_type.value for p_type in FlowModes]) + flow_action = StringField(default=FlowActionTypes.navigate.value, + choices=[p_type.value for p_type in FlowModes]) + flow_token = StringField(required=True) + recipient_phone = EmbeddedDocumentField(CustomActionRequestParameters) + initial_screen = StringField(required=True) + flow_cta = StringField(required=True) + dispatch_response = BooleanField(default=True) + response = StringField(default=None) + bot = StringField(required=True) + user = StringField(required=True) + timestamp = DateTimeField(default=datetime.utcnow) + status = BooleanField(default=True) + + def validate(self, clean=True): + if clean: + self.clean() + + if Utility.check_empty_string(self.name): + raise ValidationError("Action name cannot be empty") + if Utility.check_empty_string(self.body): + raise ValidationError("body cannot be empty") + if Utility.check_empty_string(self.initial_screen): + raise ValidationError("initial_screen cannot be empty") + if Utility.check_empty_string(self.flow_cta): + raise ValidationError("flow_cta cannot be empty") + if Utility.check_empty_string(self.flow_token): + raise ValidationError("flow_token cannot be empty") + + @auditlogger.log @push_notification.apply class GoogleSearchAction(Auditlog): diff --git a/kairon/shared/actions/models.py b/kairon/shared/actions/models.py index 51e212976..87c73696c 100644 --- a/kairon/shared/actions/models.py +++ b/kairon/shared/actions/models.py @@ -20,6 +20,16 @@ class ActionParameterType(str, Enum): key_vault = "key_vault" +class FlowModes(str, Enum): + draft = "draft" + published = "published" + + +class FlowActionTypes(str, Enum): + navigate = "navigate" + data_exchange = "data_exchange" + + class EvaluationType(str, Enum): expression = "expression" script = "script" @@ -48,6 +58,7 @@ class ActionType(str, Enum): database_action = "database_action" web_search_action = "web_search_action" live_agent_action = "live_agent_action" + flow_action = "flow_action" class HttpRequestContentType(str, Enum): diff --git a/kairon/shared/actions/utils.py b/kairon/shared/actions/utils.py index 00911f55c..969151a45 100644 --- a/kairon/shared/actions/utils.py +++ b/kairon/shared/actions/utils.py @@ -941,6 +941,57 @@ def prepare_hubspot_form_request(tracker, fields: list, bot: Text): def get_basic_auth_str(username: Text, password: Text): return requests.auth._basic_auth_str(username, password) + @staticmethod + def prepare_flow_body(flow_action_config: dict, tracker: Tracker): + api_key = Sysadmin.get_bot_secret(flow_action_config['bot'], BotSecretType.d360_api_key.value, raise_err=False) + http_url = Utility.environment["flow"]["url"] + header_key = Utility.environment["flow"]["headers"]["key"] + headers = {header_key: api_key} + flow_body = { + "recipient_type": "individual", + "messaging_product": "whatsapp", + "type": "interactive", + "interactive": { + "type": "flow", + "action": { + "name": "flow", + "parameters": { + "mode": "published", + "flow_message_version": "3", + "flow_token": "AQAAAAACS5FpgQ_cAAAAAD0QI3s.", + "flow_action": "navigate", + } + } + } + } + parameter_type = flow_action_config['recipient_phone']['parameter_type'] + recipient_phone = tracker.get_slot(flow_action_config['recipient_phone']['value']) \ + if parameter_type == 'slot' else flow_action_config['recipient_phone']['value'] + parameter_type = flow_action_config['flow_id']['parameter_type'] + flow_id = tracker.get_slot(flow_action_config['flow_id']['value']) \ + if parameter_type == 'slot' else flow_action_config['flow_id']['value'] + header = flow_action_config.get('header') + body = flow_action_config['body'] + footer = flow_action_config.get('footer') + mode = flow_action_config['mode'] + flow_action = flow_action_config['flow_action'] + flow_token = flow_action_config['flow_token'] + initial_screen = flow_action_config['initial_screen'] + flow_cta = flow_action_config['flow_cta'] + flow_body["to"] = recipient_phone + if header: + flow_body["interactive"]["header"] = {"type": "text", "text": header} + flow_body["interactive"]["body"] = {"text": body} + if footer: + flow_body["interactive"]["footer"] = {"text": footer} + flow_body["interactive"]["action"]["parameters"]["mode"] = mode + flow_body["interactive"]["action"]["parameters"]["flow_action"] = flow_action + flow_body["interactive"]["action"]["parameters"]["flow_token"] = flow_token + flow_body["interactive"]["action"]["parameters"]["flow_cta"] = flow_cta + flow_body["interactive"]["action"]["parameters"]["flow_id"] = flow_id + flow_body["interactive"]["action"]["parameters"]["flow_action_payload"] = {"screen": initial_screen} + return flow_body, http_url, headers + @staticmethod def evaluate_script(script: Text, data: Any, raise_err_on_failure: bool = True): log = [f"evaluation_type: script", f"script: {script}", f"data: {data}", f"raise_err_on_failure: {raise_err_on_failure}"] diff --git a/kairon/shared/admin/constants.py b/kairon/shared/admin/constants.py index 22eb115da..e5d69802c 100644 --- a/kairon/shared/admin/constants.py +++ b/kairon/shared/admin/constants.py @@ -3,3 +3,4 @@ class BotSecretType(str, Enum): gpt_key = "gpt_key" + d360_api_key = "d360_api_key" diff --git a/kairon/shared/data/processor.py b/kairon/shared/data/processor.py index f14ed3977..afa76e5db 100644 --- a/kairon/shared/data/processor.py +++ b/kairon/shared/data/processor.py @@ -81,6 +81,7 @@ PyscriptActionConfig, WebSearchAction, UserQuestion, LiveAgentActionConfig, + FlowActionConfig ) from kairon.shared.actions.models import ( ActionType, @@ -3904,6 +3905,77 @@ def list_http_actions(self, bot: str): actions = HttpActionConfig.objects(bot=bot, status=True) return list(self.__prepare_document_list(actions, "action_name")) + def add_flow_action(self, flow_config: Dict, user: str, bot: str): + """ + Adds a new FlowActionConfig action. + :param flow_config: dict object containing configuration for the Flow action + :param user: user id + :param bot: bot id + :return: flow configuration id for saved flow action config + """ + from kairon.shared.chat.data_objects import Channels + + flow_config['user'] = user + flow_config['bot'] = bot + if not Utility.is_exist(Channels, raise_error=False, bot=bot, connector_type="whatsapp"): + raise AppException('Whatsapp Channel not found for this bot!') + Utility.is_valid_action_name(flow_config.get("name"), bot, FlowActionConfig) + action_id = FlowActionConfig(**flow_config).save().id.__str__() + self.add_action(flow_config['name'], bot, user, action_type=ActionType.flow_action.value, + raise_exception=False) + return action_id + + def update_flow_action(self, request_data: Dict, user: str, bot: str): + """ + Updates Flow Action configuration. + :param request_data: Dict containing configuration to be modified + :param user: user id + :param bot: bot id + :return: Flow Action configuration id for updated Flow action config + """ + + if not Utility.is_exist(FlowActionConfig, raise_error=False, name=request_data.get('name'), + bot=bot, status=True): + raise AppException(f'Action with name "{request_data.get("name")}" not found') + action = FlowActionConfig.objects(name=request_data.get('name'), bot=bot, status=True).get() + action.name = request_data['name'] + action.flow_id = CustomActionRequestParameters(**request_data['flow_id']) if request_data.get( + 'flow_id') else None + action.header = request_data.get('header', None) + action.body = request_data['body'] + action.footer = request_data.get('footer', None) + action.mode = request_data.get('mode') + action.flow_action = request_data.get('flow_action') + action.flow_token = request_data.get('flow_token') + action.recipient_phone = CustomActionRequestParameters(**request_data['recipient_phone']) if request_data.get( + 'recipient_phone') else None + action.initial_screen = request_data['initial_screen'] + action.flow_cta = request_data['flow_cta'] + action.dispatch_response = request_data.get('dispatch_response', True) + action.response = request_data['response'] + action.user = user + action.timestamp = datetime.utcnow() + return action.save().id.__str__() + + def list_flow_actions(self, bot: str, with_doc_id: bool = True): + """ + Fetches all Flow actions from collection + :param bot: bot id + :param with_doc_id: return document id along with action configuration if True + :return: List of Flow actions. + """ + for action in FlowActionConfig.objects(bot=bot, status=True): + action = action.to_mongo().to_dict() + if with_doc_id: + action['_id'] = action['_id'].__str__() + else: + action.pop('_id') + action.pop('user') + action.pop('bot') + action.pop('status') + action.pop('timestamp') + yield action + def add_pyscript_action(self, pyscript_config: Dict, user: str, bot: str): """ Adds a new PyscriptActionConfig action. @@ -6450,6 +6522,8 @@ def delete_action(self, name: Text, bot: Text, user: Text): Utility.hard_delete_document( [PyscriptActionConfig], name__iexact=name, bot=bot ) + elif action.type == ActionType.flow_action.value: + Utility.delete_document([FlowActionConfig], name__iexact=name, bot=bot, user=user) action.delete() except DoesNotExist: raise AppException(f'Action with name "{name}" not found') diff --git a/system.yaml b/system.yaml index de197260b..cbd15ee9b 100644 --- a/system.yaml +++ b/system.yaml @@ -151,6 +151,11 @@ evaluator: trigger_task: ${PYSCRIPT_TRIGGER_TASK:false} url: ${PYSCRIPT_EVALUATOR_ENDPOINT:"http://192.168.100.109:8087/evaluate"} +flow: + url: ${FLOW_TRIGGERING_ENDPOINT:"https://waba-v2.360dialog.io/messages"} + headers: + key: ${FLOW_HEADERS_KEY:"D360-API-KEY"} + multilingual: enable: ${ENABLE_MULTILINGUAL_BOTS:false} project_id: ${MULTILINGUAL_TRANSLATOR_PROJECT_ID} diff --git a/tests/integration_test/action_service_test.py b/tests/integration_test/action_service_test.py index 77f3c0262..3fe018d41 100644 --- a/tests/integration_test/action_service_test.py +++ b/tests/integration_test/action_service_test.py @@ -19,7 +19,8 @@ EmailActionConfig, ActionServerLogs, GoogleSearchAction, JiraAction, ZendeskAction, PipedriveLeadsAction, SetSlots, \ HubspotFormsAction, HttpActionResponse, HttpActionRequestBody, SetSlotsFromResponse, CustomActionRequestParameters, \ KaironTwoStageFallbackAction, TwoStageFallbackTextualRecommendations, RazorpayAction, PromptAction, FormSlotSet, \ - DatabaseAction, DbQuery, PyscriptActionConfig, WebSearchAction, UserQuestion, LiveAgentActionConfig + DatabaseAction, DbQuery, PyscriptActionConfig, WebSearchAction, UserQuestion, LiveAgentActionConfig, \ + FlowActionConfig from kairon.shared.actions.models import ActionType, ActionParameterType, DispatchType, DbActionOperationType, \ DbQueryValueType from kairon.shared.actions.utils import ActionUtility @@ -496,6 +497,477 @@ def test_live_agent_action_execution_with_exception(aioresponses): assert response_json == {'events': [], 'responses': [{'text': 'Connecting to live agent', 'buttons': [], 'elements': [], 'custom': {}, 'template': None, 'response': None, 'image': None, 'attachment': None}]} +@responses.activate +def test_flow_action_execution(aioresponses): + action_name = "test_flow_action_execution" + Actions(name=action_name, type=ActionType.flow_action.value, + bot="5f50fd0a56b698ca10d35d2z", user="user").save() + BotSecrets(secret_type=BotSecretType.d360_api_key.value, + value="abxyYxCNkabcdefgh9OpMfIAK", bot="5f50fd0a56b698ca10d35d2z", user="user").save() + FlowActionConfig( + name=action_name, + flow_id=CustomActionRequestParameters(value="9191123456789", + parameter_type=ActionParameterType.value.value), + body="Fill the Sign Up Form", + recipient_phone=CustomActionRequestParameters(value="919876543210", + parameter_type=ActionParameterType.value.value), + initial_screen="REGISTER", + flow_token="AHSKSSLLHLSKLSKS", + flow_cta="Sign Up", + response="Flow Triggered", + bot="5f50fd0a56b698ca10d35d2z", + user="user" + ).save() + + resp_msg = json.dumps({ + "messaging_product": "whatsapp", + "contacts": [ + { + "input": "919876543210", + "wa_id": "919876543210" + } + ], + "messages": [ + { + "id": "wamid.HBgMOTE5NTEsTdlFsldsGldlsHfsfndMEIxQTMwMjQ1MzRDQTdEAA==" + } + ] + }) + + aioresponses.add( + method=responses.POST, + url="https://waba-v2.360dialog.io/messages", + body=resp_msg, + status=200 + ) + + request_object = { + "next_action": action_name, + "tracker": { + "sender_id": "default", + "conversation_id": "default", + "slots": {"bot": "5f50fd0a56b698ca10d35d2z", "location": "Bangalore", "langauge": "Kannada"}, + "latest_message": {'text': 'get intents', 'intent_ranking': [{'name': 'flow_action'}]}, + "latest_event_time": 1537645578.314389, + "followup_action": "action_listen", + "paused": False, + "events": [{"event1": "hello"}, {"event2": "how are you"}], + "latest_input_channel": "rest", + "active_loop": {}, + "latest_action": {}, + }, + "domain": { + "config": {}, + "session_config": {}, + "intents": [], + "entities": [], + "slots": {"bot": "5f50fd0a56b698ca10d35d2z"}, + "responses": {}, + "actions": [], + "forms": {}, + "e2e_actions": [] + }, + "version": "version" + } + response = client.post("/webhook", json=request_object) + response_json = response.json() + assert response.status_code == 200 + assert len(response_json['events']) == 1 + assert len(response_json['responses']) == 1 + assert response_json['events'] == [ + {'event': 'slot', 'timestamp': None, 'name': 'kairon_action_response', 'value': 'Flow Triggered'} + ] + assert response_json['responses'][0]['text'] == 'Flow Triggered' + log = ActionServerLogs.objects(action=action_name).get().to_mongo().to_dict() + log.pop("_id") + log.pop("timestamp") + log['headers'].pop('X-TimeStamp') + assert log == {'type': 'flow_action', 'intent': 'flow_action', 'action': 'test_flow_action_execution', + 'sender': 'default', 'headers': {'D360-API-KEY': 'abxyYxCNkabcdefgh9OpMfIAK'}, + 'url': 'https://waba-v2.360dialog.io/messages', + 'request_params': {'recipient_type': 'individual', 'messaging_product': 'whatsapp', 'type': 'interactive', 'interactive': {'type': 'flow', 'action': {'name': 'flow', 'parameters': {'mode': 'published', 'flow_message_version': '3', 'flow_token': 'AHSKSSLLHLSKLSKS', 'flow_action': 'navigate', 'flow_cta': 'Sign Up', 'flow_id': '9191123456789', 'flow_action_payload': {'screen': 'REGISTER'}}}, 'body': {'text': 'Fill the Sign Up Form'}}, 'to': '919876543210'}, + 'api_response': "{'messaging_product': 'whatsapp', 'contacts': [{'input': '919876543210', 'wa_id': '919876543210'}], 'messages': [{'id': 'wamid.HBgMOTE5NTEsTdlFsldsGldlsHfsfndMEIxQTMwMjQ1MzRDQTdEAA=='}]}", + 'bot_response': 'Flow Triggered', 'bot': '5f50fd0a56b698ca10d35d2z', 'status': 'SUCCESS', + 'user_msg': 'get intents'} + + +@responses.activate +def test_flow_action_execution_with_flow_id_from_slot(aioresponses): + action_name = "test_flow_action_execution_with_flow_id_from_slot" + Actions(name=action_name, type=ActionType.flow_action.value, + bot="5f50fd0a56b698ca10d35d2z", user="user").save() + FlowActionConfig( + name=action_name, + flow_id=CustomActionRequestParameters(value="flow_id", + parameter_type=ActionParameterType.slot.value), + body="Fill the Sign Up Form", + recipient_phone=CustomActionRequestParameters(value="919876543210", + parameter_type=ActionParameterType.value.value), + initial_screen="REGISTER", + flow_token="AHSKSSLLHLSKLSKS", + flow_action="data_exchange", + flow_cta="Sign Up", + response="Flow Triggered", + bot="5f50fd0a56b698ca10d35d2z", + user="user" + ).save() + + resp_msg = json.dumps({ + "messaging_product": "whatsapp", + "contacts": [ + { + "input": "919876543210", + "wa_id": "919876543210" + } + ], + "messages": [ + { + "id": "wamid.HBgMOTE5NTEsTdlFsldsGldlsHfsfndMEIxQTMwMjQ1MzRDQTdEAA==" + } + ] + }) + + aioresponses.add( + method=responses.POST, + url="https://waba-v2.360dialog.io/messages", + body=resp_msg, + status=200 + ) + + request_object = { + "next_action": action_name, + "tracker": { + "sender_id": "default", + "conversation_id": "default", + "slots": {"bot": "5f50fd0a56b698ca10d35d2z", "flow_id": "919112345678900303", "langauge": "Kannada"}, + "latest_message": {'text': 'get intents', 'intent_ranking': [{'name': 'flow_action'}]}, + "latest_event_time": 1537645578.314389, + "followup_action": "action_listen", + "paused": False, + "events": [{"event1": "hello"}, {"event2": "how are you"}], + "latest_input_channel": "rest", + "active_loop": {}, + "latest_action": {}, + }, + "domain": { + "config": {}, + "session_config": {}, + "intents": [], + "entities": [], + "slots": {"bot": "5f50fd0a56b698ca10d35d2z"}, + "responses": {}, + "actions": [], + "forms": {}, + "e2e_actions": [] + }, + "version": "version" + } + response = client.post("/webhook", json=request_object) + response_json = response.json() + assert response.status_code == 200 + assert len(response_json['events']) == 1 + assert len(response_json['responses']) == 1 + assert response_json['events'] == [ + {'event': 'slot', 'timestamp': None, 'name': 'kairon_action_response', 'value': 'Flow Triggered'} + ] + assert response_json['responses'][0]['text'] == 'Flow Triggered' + log = ActionServerLogs.objects(action=action_name).get().to_mongo().to_dict() + log.pop("_id") + log.pop("timestamp") + log['headers'].pop('X-TimeStamp') + assert log == {'type': 'flow_action', 'intent': 'flow_action', + 'action': 'test_flow_action_execution_with_flow_id_from_slot', + 'sender': 'default', 'headers': {'D360-API-KEY': 'abxyYxCNkabcdefgh9OpMfIAK'}, + 'url': 'https://waba-v2.360dialog.io/messages', + 'request_params': {'recipient_type': 'individual', 'messaging_product': 'whatsapp', 'type': 'interactive', 'interactive': {'type': 'flow', 'action': {'name': 'flow', 'parameters': {'mode': 'published', 'flow_message_version': '3', 'flow_token': 'AHSKSSLLHLSKLSKS', 'flow_action': 'data_exchange', 'flow_cta': 'Sign Up', 'flow_id': '919112345678900303', 'flow_action_payload': {'screen': 'REGISTER'}}}, 'body': {'text': 'Fill the Sign Up Form'}}, 'to': '919876543210'}, + 'api_response': "{'messaging_product': 'whatsapp', 'contacts': [{'input': '919876543210', 'wa_id': '919876543210'}], 'messages': [{'id': 'wamid.HBgMOTE5NTEsTdlFsldsGldlsHfsfndMEIxQTMwMjQ1MzRDQTdEAA=='}]}", + 'bot_response': 'Flow Triggered', 'bot': '5f50fd0a56b698ca10d35d2z', 'status': 'SUCCESS', + 'user_msg': 'get intents'} + + +@responses.activate +def test_flow_action_execution_with_recipient_phone_from_slot(aioresponses): + action_name = "test_flow_action_execution_with_recipient_phone_from_slot" + Actions(name=action_name, type=ActionType.flow_action.value, + bot="5f50fd0a56b698ca10d35d2z", user="user").save() + FlowActionConfig( + name=action_name, + flow_id=CustomActionRequestParameters(value="9191123456789", + parameter_type=ActionParameterType.value.value), + body="Fill the Sign Up Form", + recipient_phone=CustomActionRequestParameters(value="phone", + parameter_type=ActionParameterType.slot.value), + initial_screen="REGISTER", + flow_cta="Sign Up", + flow_token="AHSKSSLLHLSKLSKS", + response="Flow Triggered", + bot="5f50fd0a56b698ca10d35d2z", + user="user" + ).save() + + resp_msg = json.dumps({ + "messaging_product": "whatsapp", + "contacts": [ + { + "input": "919877766554", + "wa_id": "919877766554" + } + ], + "messages": [ + { + "id": "wamid.HBgMOTE5NTEsTdlFsldsGldlsHfsfndMEIxQTMwMjQ1MzRDQTdEAA==" + } + ] + }) + + aioresponses.add( + method=responses.POST, + url="https://waba-v2.360dialog.io/messages", + body=resp_msg, + status=200 + ) + + request_object = { + "next_action": action_name, + "tracker": { + "sender_id": "default", + "conversation_id": "default", + "slots": {"bot": "5f50fd0a56b698ca10d35d2z", "phone": "919877766554", "langauge": "Kannada"}, + "latest_message": {'text': 'get intents', 'intent_ranking': [{'name': 'flow_action'}]}, + "latest_event_time": 1537645578.314389, + "followup_action": "action_listen", + "paused": False, + "events": [{"event1": "hello"}, {"event2": "how are you"}], + "latest_input_channel": "rest", + "active_loop": {}, + "latest_action": {}, + }, + "domain": { + "config": {}, + "session_config": {}, + "intents": [], + "entities": [], + "slots": {"bot": "5f50fd0a56b698ca10d35d2z"}, + "responses": {}, + "actions": [], + "forms": {}, + "e2e_actions": [] + }, + "version": "version" + } + response = client.post("/webhook", json=request_object) + response_json = response.json() + assert response.status_code == 200 + assert len(response_json['events']) == 1 + assert len(response_json['responses']) == 1 + assert response_json['events'] == [ + {'event': 'slot', 'timestamp': None, 'name': 'kairon_action_response', 'value': 'Flow Triggered'} + ] + assert response_json['responses'][0]['text'] == 'Flow Triggered' + log = ActionServerLogs.objects(action=action_name).get().to_mongo().to_dict() + log.pop("_id") + log.pop("timestamp") + log['headers'].pop('X-TimeStamp') + assert log == {'type': 'flow_action', 'intent': 'flow_action', + 'action': 'test_flow_action_execution_with_recipient_phone_from_slot', + 'sender': 'default', 'headers': {'D360-API-KEY': 'abxyYxCNkabcdefgh9OpMfIAK'}, + 'url': 'https://waba-v2.360dialog.io/messages', + 'request_params': {'recipient_type': 'individual', 'messaging_product': 'whatsapp', 'type': 'interactive', 'interactive': {'type': 'flow', 'action': {'name': 'flow', 'parameters': {'mode': 'published', 'flow_message_version': '3', 'flow_token': 'AHSKSSLLHLSKLSKS', 'flow_action': 'navigate', 'flow_cta': 'Sign Up', 'flow_id': '9191123456789', 'flow_action_payload': {'screen': 'REGISTER'}}}, 'body': {'text': 'Fill the Sign Up Form'}}, 'to': '919877766554'}, + 'api_response': "{'messaging_product': 'whatsapp', 'contacts': [{'input': '919877766554', 'wa_id': '919877766554'}], 'messages': [{'id': 'wamid.HBgMOTE5NTEsTdlFsldsGldlsHfsfndMEIxQTMwMjQ1MzRDQTdEAA=='}]}", + 'bot_response': 'Flow Triggered', 'bot': '5f50fd0a56b698ca10d35d2z', 'status': 'SUCCESS', + 'user_msg': 'get intents'} + + +@responses.activate +def test_flow_action_execution_with_header_footer(aioresponses): + action_name = "test_flow_action_execution_with_header_footer" + Actions(name=action_name, type=ActionType.flow_action.value, + bot="5f50fd0a56b698ca10d35d2z", user="user").save() + FlowActionConfig( + name=action_name, + flow_id=CustomActionRequestParameters(value="9191123456789", + parameter_type=ActionParameterType.value.value), + body="Fill the Sign Up Form", + header="This is flow header", + footer="This is flow footer", + recipient_phone=CustomActionRequestParameters(value="919876543210", + parameter_type=ActionParameterType.value.value), + initial_screen="REGISTER", + flow_token="AHSKSSLLHLSKLSKS", + flow_cta="Sign Up", + response="Flow Triggered", + bot="5f50fd0a56b698ca10d35d2z", + user="user" + ).save() + + resp_msg = json.dumps({ + "messaging_product": "whatsapp", + "contacts": [ + { + "input": "919876543210", + "wa_id": "919876543210" + } + ], + "messages": [ + { + "id": "wamid.HBgMOTE5NTEsTdlFsldsGldlsHfsfndMEIxQTMwMjQ1MzRDQTdEAA==" + } + ] + }) + + aioresponses.add( + method=responses.POST, + url="https://waba-v2.360dialog.io/messages", + body=resp_msg, + status=200 + ) + + request_object = { + "next_action": action_name, + "tracker": { + "sender_id": "default", + "conversation_id": "default", + "slots": {"bot": "5f50fd0a56b698ca10d35d2z", "location": "Bangalore", "langauge": "Kannada"}, + "latest_message": {'text': 'get intents', 'intent_ranking': [{'name': 'flow_action'}]}, + "latest_event_time": 1537645578.314389, + "followup_action": "action_listen", + "paused": False, + "events": [{"event1": "hello"}, {"event2": "how are you"}], + "latest_input_channel": "rest", + "active_loop": {}, + "latest_action": {}, + }, + "domain": { + "config": {}, + "session_config": {}, + "intents": [], + "entities": [], + "slots": {"bot": "5f50fd0a56b698ca10d35d2z"}, + "responses": {}, + "actions": [], + "forms": {}, + "e2e_actions": [] + }, + "version": "version" + } + response = client.post("/webhook", json=request_object) + response_json = response.json() + assert response.status_code == 200 + assert len(response_json['events']) == 1 + assert len(response_json['responses']) == 1 + assert response_json['events'] == [ + {'event': 'slot', 'timestamp': None, 'name': 'kairon_action_response', 'value': 'Flow Triggered'} + ] + assert response_json['responses'][0]['text'] == 'Flow Triggered' + log = ActionServerLogs.objects(action=action_name).get().to_mongo().to_dict() + log.pop("_id") + log.pop("timestamp") + log['headers'].pop('X-TimeStamp') + assert log == {'type': 'flow_action', 'intent': 'flow_action', + 'action': 'test_flow_action_execution_with_header_footer', + 'sender': 'default', 'headers': {'D360-API-KEY': 'abxyYxCNkabcdefgh9OpMfIAK'}, + 'url': 'https://waba-v2.360dialog.io/messages', + 'request_params': {'recipient_type': 'individual', 'messaging_product': 'whatsapp', 'type': 'interactive', 'interactive': {'type': 'flow', 'action': {'name': 'flow', 'parameters': {'mode': 'published', 'flow_message_version': '3', 'flow_token': 'AHSKSSLLHLSKLSKS', 'flow_action': 'navigate', 'flow_cta': 'Sign Up', 'flow_id': '9191123456789', 'flow_action_payload': {'screen': 'REGISTER'}}}, 'header': {'type': 'text', 'text': 'This is flow header'}, 'body': {'text': 'Fill the Sign Up Form'}, 'footer': {'text': 'This is flow footer'}}, 'to': '919876543210'}, + 'api_response': "{'messaging_product': 'whatsapp', 'contacts': [{'input': '919876543210', 'wa_id': '919876543210'}], 'messages': [{'id': 'wamid.HBgMOTE5NTEsTdlFsldsGldlsHfsfndMEIxQTMwMjQ1MzRDQTdEAA=='}]}", + 'bot_response': 'Flow Triggered', 'bot': '5f50fd0a56b698ca10d35d2z', 'status': 'SUCCESS', + 'user_msg': 'get intents'} + + +@responses.activate +def test_flow_action_execution_with_dispatch_response_disabled(aioresponses): + action_name = "test_flow_action_execution_with_dispatch_response_disabled" + Actions(name=action_name, type=ActionType.flow_action.value, + bot="5f50fd0a56b698ca10d35d2z", user="user").save() + FlowActionConfig( + name=action_name, + flow_id=CustomActionRequestParameters(value="9191123498765432", + parameter_type=ActionParameterType.value.value), + body="Fill the Sign Up Form", + header="This is flow header", + footer="This is flow footer", + mode="draft", + recipient_phone=CustomActionRequestParameters(value="919876543210", + parameter_type=ActionParameterType.value.value), + initial_screen="REGISTER", + flow_cta="Sign Up", + flow_token="AHSKSSLLHLSKLSKS", + response="Flow Triggered", + dispatch_response=False, + bot="5f50fd0a56b698ca10d35d2z", + user="user" + ).save() + + resp_msg = json.dumps({ + "messaging_product": "whatsapp", + "contacts": [ + { + "input": "919876543210", + "wa_id": "919876543210" + } + ], + "messages": [ + { + "id": "wamid.HBgMOTE5NTEsTdlFsldsGldlsHfsfndMEIxQTMwMjQ1MzRDQTdEAA==" + } + ] + }) + + aioresponses.add( + method=responses.POST, + url="https://waba-v2.360dialog.io/messages", + body=resp_msg, + status=200 + ) + + request_object = { + "next_action": action_name, + "tracker": { + "sender_id": "default", + "conversation_id": "default", + "slots": {"bot": "5f50fd0a56b698ca10d35d2z", "location": "Bangalore", "langauge": "Kannada"}, + "latest_message": {'text': 'get intents', 'intent_ranking': [{'name': 'flow_action'}]}, + "latest_event_time": 1537645578.314389, + "followup_action": "action_listen", + "paused": False, + "events": [{"event1": "hello"}, {"event2": "how are you"}], + "latest_input_channel": "rest", + "active_loop": {}, + "latest_action": {}, + }, + "domain": { + "config": {}, + "session_config": {}, + "intents": [], + "entities": [], + "slots": {"bot": "5f50fd0a56b698ca10d35d2z"}, + "responses": {}, + "actions": [], + "forms": {}, + "e2e_actions": [] + }, + "version": "version" + } + response = client.post("/webhook", json=request_object) + response_json = response.json() + assert response.status_code == 200 + assert len(response_json['events']) == 1 + assert len(response_json['responses']) == 0 + assert response_json['responses'] == [] + assert response_json['events'] == [ + {'event': 'slot', 'timestamp': None, 'name': 'kairon_action_response', 'value': 'Flow Triggered'} + ] + log = ActionServerLogs.objects(action=action_name).get().to_mongo().to_dict() + log.pop("_id") + log.pop("timestamp") + log['headers'].pop('X-TimeStamp') + assert log == {'type': 'flow_action', 'intent': 'flow_action', + 'action': 'test_flow_action_execution_with_dispatch_response_disabled', + 'sender': 'default', 'headers': {'D360-API-KEY': 'abxyYxCNkabcdefgh9OpMfIAK'}, + 'url': 'https://waba-v2.360dialog.io/messages', + 'request_params': {'recipient_type': 'individual', 'messaging_product': 'whatsapp', 'type': 'interactive', 'interactive': {'type': 'flow', 'action': {'name': 'flow', 'parameters': {'mode': 'draft', 'flow_message_version': '3', 'flow_token': 'AHSKSSLLHLSKLSKS', 'flow_action': 'navigate', 'flow_cta': 'Sign Up', 'flow_id': '9191123498765432', 'flow_action_payload': {'screen': 'REGISTER'}}}, 'header': {'type': 'text', 'text': 'This is flow header'}, 'body': {'text': 'Fill the Sign Up Form'}, 'footer': {'text': 'This is flow footer'}}, 'to': '919876543210'}, + 'api_response': "{'messaging_product': 'whatsapp', 'contacts': [{'input': '919876543210', 'wa_id': '919876543210'}], 'messages': [{'id': 'wamid.HBgMOTE5NTEsTdlFsldsGldlsHfsfndMEIxQTMwMjQ1MzRDQTdEAA=='}]}", + 'bot_response': 'Flow Triggered', 'bot': '5f50fd0a56b698ca10d35d2z', 'status': 'SUCCESS', + 'user_msg': 'get intents'} @responses.activate diff --git a/tests/integration_test/services_test.py b/tests/integration_test/services_test.py index d5d66fd5a..f3026011a 100644 --- a/tests/integration_test/services_test.py +++ b/tests/integration_test/services_test.py @@ -1557,6 +1557,592 @@ def test_upload_with_bot_content_event_append_validate_payload_data(): bot_settings.save() +def test_add_flow_action_empty_name(): + request_body = { + "name": "", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.post( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == [{'loc': ['body', 'name'], 'msg': 'name is required', 'type': 'value_error'}] + assert not actual["success"] + + +def test_add_flow_action_empty_body(): + request_body = { + "name": "test_add_flow_action_empty_body", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.post( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == [{'loc': ['body', 'body'], 'msg': 'body is required', 'type': 'value_error'}] + assert not actual["success"] + + +def test_add_flow_action_empty_initial_screen(): + request_body = { + "name": "test_add_flow_action_empty_initial_screen", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.post( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == [{'loc': ['body', 'initial_screen'], + 'msg': 'initial_screen is required', 'type': 'value_error'}] + assert not actual["success"] + + +def test_add_flow_action_empty_flow_cta(): + request_body = { + "name": "test_add_flow_action_empty_flow_cta", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "", + "response": "Test Response", + } + response = client.post( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == [{'loc': ['body', 'flow_cta'], 'msg': 'flow_cta is required', 'type': 'value_error'}] + assert not actual["success"] + + +def test_add_flow_action_with_invalid_mode(): + request_body = { + "name": "test_add_flow", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "invalid_mode", + "flow_token": "AHSKSSLLHLSKLSKS", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.post( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert not actual["success"] + + +def test_add_flow_action_empty_flow_token(): + request_body = { + "name": "flow_action", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.post( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == [{'loc': ['body', 'flow_token'], 'msg': 'flow_token is required', + 'type': 'value_error'}] + assert not actual["success"] + + +def test_add_flow_action_invalid_flow_action(): + request_body = { + "name": "flow_action", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "invalid", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.post( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert not actual["success"] + + +def test_add_flow_action_without_whatsapp_channel(): + request_body = { + "name": "flow_action", + "flow_id": {"value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "recipient_phone": {'value': "919911837465"}, + "initial_screen": "REGISTER", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "data_exchange", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.post( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == "Whatsapp Channel not found for this bot!" + assert not actual["success"] + + +def test_add_whatsapp_channel(monkeypatch): + data = {"connector_type": "whatsapp", + "config": { + "access_token": "xoxb-801939352912-801478018484-v3zq6MYNu62oSs8vammWOY8K", + "app_secret": "79f036b9894eef17c064213b90d1042b", + "verify_token": "3396830255712.3396861654876869879", + }} + response = client.post( + f"/api/bot/{pytest.bot}/channels/add", + json=data, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert actual["success"] + assert actual["error_code"] == 0 + assert actual["message"] == "Channel added" + + +def test_add_flow_action(): + request_body = { + "name": "flow_action", + "flow_id": {"value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "recipient_phone": {'value': "919911837465"}, + "initial_screen": "REGISTER", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "data_exchange", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.post( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 0 + assert actual["message"] == "Action added!" + assert actual["success"] + + +def test_add_flow_action_with_name_exist(): + request_body = { + "name": "flow_action", + "flow_id": {"parameter_type": "slot", "value": "flow_id"}, + "body": "Fill the Sign Up Form", + "mode": "published", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "data_exchange", + "recipient_phone": {'value': "phone", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.post( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == "Action exists!" + assert not actual["data"] + assert not actual["success"] + + +def test_add_flow_action_with_slot_values(): + request_body = { + "name": "flow_action_with_slot_values", + "flow_id": {"parameter_type": "slot", "value": "flow_id"}, + "body": "Fill the Form to Book", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "navigate", + "recipient_phone": {'value': "phone", "parameter_type": "slot"}, + "initial_screen": "BOOKING", + "flow_cta": "Book Now", + "response": "Flow Triggered", + "dispatch_response": False + } + response = client.post( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 0 + assert actual["message"] == "Action added!" + assert actual["success"] + assert actual["data"] + + +def test_get_flow_actions(): + response = client.get( + url=f"/api/bot/{pytest.bot}/action/flow", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 0 + assert actual["success"] + assert len(actual["data"]) == 2 + assert actual["data"][0]["name"] == "flow_action" + assert actual["data"][0]["flow_id"] == {'_cls': 'CustomActionRequestParameters', 'encrypt': False, + 'value': '9191123456789', 'parameter_type': 'value'} + assert actual["data"][0]["body"] == "Fill the Sign Up Form" + assert actual["data"][0]["mode"] == "published" + assert actual["data"][0]["flow_action"] == "data_exchange" + assert actual["data"][0]["flow_token"] == "AHSKSSLLHLSKLSKS" + assert actual["data"][0]["recipient_phone"] == {'_cls': 'CustomActionRequestParameters', 'encrypt': False, + 'value': '919911837465', 'parameter_type': 'value'} + assert actual["data"][0]["initial_screen"] == "REGISTER" + assert actual["data"][0]["flow_cta"] == "Sign Up" + assert actual["data"][0]["dispatch_response"] + assert actual["data"][0]["response"] == "Test Response" + assert actual["data"][1]["name"] == "flow_action_with_slot_values" + assert actual["data"][1]["flow_id"] == {'_cls': 'CustomActionRequestParameters', 'encrypt': False, + 'value': 'flow_id', 'parameter_type': 'slot'} + assert actual["data"][1]["body"] == "Fill the Form to Book" + assert actual["data"][1]["mode"] == "draft" + assert actual["data"][1]["flow_action"] == "navigate" + assert actual["data"][1]["flow_token"] == "AHSKSSLLHLSKLSKS" + assert actual["data"][1]["recipient_phone"] == {'_cls': 'CustomActionRequestParameters', 'encrypt': False, + 'value': 'phone', 'parameter_type': 'slot'} + assert actual["data"][1]["initial_screen"] == "BOOKING" + assert actual["data"][1]["flow_cta"] == "Book Now" + assert not actual["data"][1]["dispatch_response"] + assert actual["data"][1]["response"] == "Flow Triggered" + + +def test_update_flow_action_empty_body(): + request_body = { + "name": "flow_action", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "navigate", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.put( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == [{'loc': ['body', 'body'], 'msg': 'body is required', 'type': 'value_error'}] + assert not actual["success"] + + +def test_update_flow_action_empty_initial_screen(): + request_body = { + "name": "flow_action", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "navigate", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.put( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == [{'loc': ['body', 'initial_screen'], + 'msg': 'initial_screen is required', 'type': 'value_error'}] + assert not actual["success"] + + +def test_update_flow_action_empty_flow_cta(): + request_body = { + "name": "flow_action", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "navigate", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "", + "response": "Test Response", + } + response = client.put( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == [{'loc': ['body', 'flow_cta'], 'msg': 'flow_cta is required', 'type': 'value_error'}] + assert not actual["success"] + + +def test_update_flow_action_with_invalid_mode(): + request_body = { + "name": "flow_action", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "invalid_mode", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "navigate", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.put( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert not actual["success"] + + +def test_update_flow_action_empty_flow_token(): + request_body = { + "name": "flow_action", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.put( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == [{'loc': ['body', 'flow_token'], 'msg': 'flow_token is required', + 'type': 'value_error'}] + assert not actual["success"] + + +def test_update_flow_action_invalid_flow_action(): + request_body = { + "name": "flow_action", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "invalid", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.put( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert not actual["success"] + + +def test_update_flow_action_does_not_exist(): + request_body = { + "name": "test_update_flow_action_doesnot_exist", + "flow_id": {"value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "navigate", + "recipient_phone": {'value': "919911837465"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + response = client.put( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == 'Action with name "test_update_flow_action_doesnot_exist" not found' + assert not actual["success"] + + +def test_update_flow_action(): + request_body = { + "name": "flow_action", + "flow_id": {"parameter_type": "slot", "value": "flow_id"}, + "body": "Fill Your Details", + "recipient_phone": {'value': "919913456772"}, + "mode": "draft", + "flow_token": "AHSKSSLLKAKLLSSLSLSSL", + "flow_action": "navigate", + "initial_screen": "DETAILS", + "flow_cta": "Fill Form", + "response": "Form Triggered", + "dispatch_response": False + } + response = client.put( + url=f"/api/bot/{pytest.bot}/action/flow", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 0 + assert actual["message"] == 'Action updated!' + assert actual["success"] + + +def test_get_flow_actions_after_updated(): + response = client.get( + url=f"/api/bot/{pytest.bot}/action/flow", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 0 + assert actual["success"] + assert len(actual["data"]) == 2 + assert actual["data"][0]["name"] == "flow_action" + assert actual["data"][0]["flow_id"] == {'_cls': 'CustomActionRequestParameters', 'encrypt': False, + 'value': 'flow_id', 'parameter_type': 'slot'} + assert actual["data"][0]["body"] == "Fill Your Details" + assert actual["data"][0]["mode"] == "draft" + assert actual["data"][0]["flow_action"] == "navigate" + assert actual["data"][0]["flow_token"] == "AHSKSSLLKAKLLSSLSLSSL" + assert actual["data"][0]["recipient_phone"] == {'_cls': 'CustomActionRequestParameters', 'encrypt': False, + 'value': '919913456772', 'parameter_type': 'value'} + assert actual["data"][0]["initial_screen"] == "DETAILS" + assert actual["data"][0]["flow_cta"] == "Fill Form" + assert not actual["data"][0]["dispatch_response"] + assert actual["data"][0]["response"] == "Form Triggered" + + assert actual["data"][1]["name"] == "flow_action_with_slot_values" + assert actual["data"][1]["flow_id"] == {'_cls': 'CustomActionRequestParameters', 'encrypt': False, + 'value': 'flow_id', 'parameter_type': 'slot'} + assert actual["data"][1]["body"] == "Fill the Form to Book" + assert actual["data"][1]["mode"] == "draft" + assert actual["data"][1]["flow_action"] == "navigate" + assert actual["data"][1]["flow_token"] == "AHSKSSLLHLSKLSKS" + assert actual["data"][1]["recipient_phone"] == {'_cls': 'CustomActionRequestParameters', 'encrypt': False, + 'value': 'phone', 'parameter_type': 'slot'} + assert actual["data"][1]["initial_screen"] == "BOOKING" + assert actual["data"][1]["flow_cta"] == "Book Now" + assert not actual["data"][1]["dispatch_response"] + assert actual["data"][1]["response"] == "Flow Triggered" + + +def test_delete_flow_action(): + response = client.delete( + f"/api/bot/{pytest.bot}/action/flow_action", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert actual["success"] + assert actual["error_code"] == 0 + assert actual["message"] == 'Action deleted' + + +def test_delete_flow_action_not_exists(): + response = client.delete( + f"/api/bot/{pytest.bot}/action/flow_action", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert not actual["success"] + assert actual["error_code"] == 422 + assert actual["message"] == 'Action with name "flow_action" not found' + + def test_get_live_agent_with_no_live_agent(): response = client.get( url=f"/api/bot/{pytest.bot}/action/live_agent", @@ -11899,7 +12485,8 @@ def test_list_actions(): 'hubspot_forms_action': [], 'two_stage_fallback': [], 'kairon_bot_response': [], 'razorpay_action': [], 'prompt_action': [], - 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': []} + 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [], + 'flow_action': []} assert actual["success"] @@ -18587,6 +19174,7 @@ def mock_reload_model(*args, **kwargs): "actions": [], "pyscript_action": [], "live_agent_action": [], + "flow_action": [] }, ignore_order=True, ) diff --git a/tests/testing_data/system.yaml b/tests/testing_data/system.yaml index 37779d22c..a58aa20e9 100644 --- a/tests/testing_data/system.yaml +++ b/tests/testing_data/system.yaml @@ -147,6 +147,11 @@ evaluator: trigger_task: ${PYSCRIPT_TRIGGER_TASK:false} url: ${PYSCRIPT_EVALUATOR_ENDPOINT:"http://localhost:8080/evaluate"} +flow: + url: ${FLOW_TRIGGERING_ENDPOINT:"https://waba-v2.360dialog.io/messages"} + headers: + key: ${FLOW_HEADERS_KEY:"D360-API-KEY"} + multilingual: enable: ${ENABLE_MULTILINGUAL_BOTS:false} project_id: ${MULTILINGUAL_TRANSLATOR_PROJECT_ID} diff --git a/tests/unit_test/data_processor/data_processor_test.py b/tests/unit_test/data_processor/data_processor_test.py index 377225ed3..cdc6551d6 100644 --- a/tests/unit_test/data_processor/data_processor_test.py +++ b/tests/unit_test/data_processor/data_processor_test.py @@ -1197,6 +1197,391 @@ def test_bot_id_change(self): bot_id = Slots.objects(bot="test_load_yml", user="testUser", influence_conversation=False, name='bot').get() assert bot_id['initial_value'] == "test_load_yml" + @patch("kairon.shared.data.utils.DataUtility.get_channel_endpoint", autospec=True) + def test_add_whatsapp_channel_config(self, mock_get_channel_endpoint): + from kairon.shared.chat.processor import ChatDataProcessor + + bot = 'test_bot' + user = 'test_user' + processor = ChatDataProcessor() + channel_endpoint = "https://test@test.com/api/bot/whatsapp/test_bot/access_token" + mock_get_channel_endpoint.return_value = channel_endpoint + data = {"connector_type": "whatsapp", + "config": { + "access_token": "xoxb-801939352912-801478018484-v3zq6MYNu62oSs8vammWOY8K", + "app_secret": "79f036b9894eef17c064213b90d1042b", + "verify_token": "3396830255712.3396861654876869879", + }} + endpoint = processor.save_channel_config(configuration=data, bot=bot, user=user) + assert endpoint == channel_endpoint + + def test_add_flow_action_empty_name(self): + bot = 'test_bot' + user = 'test_user' + action = "test_add_flow_action_empty_name" + processor = MongoProcessor() + flow_config_dict = { + "name": "", + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + with pytest.raises(ValidationError, match="Action name cannot be empty"): + processor.add_flow_action(flow_config_dict, user, bot) + + def test_add_flow_action_empty_body(self): + bot = 'test_bot' + user = 'test_user' + action = "test_add_flow_action_empty_name" + processor = MongoProcessor() + flow_config_dict = { + "name": action, + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + with pytest.raises(ValidationError, match="body cannot be empty"): + processor.add_flow_action(flow_config_dict, user, bot) + + def test_add_flow_action_empty_initial_screen(self): + bot = 'test_bot' + user = 'test_user' + action = "test_add_flow_action_empty_name" + processor = MongoProcessor() + flow_config_dict = { + "name": action, + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "", + "flow_cta": "Sign Up", + "response": "Test Response", + } + with pytest.raises(ValidationError, match="initial_screen cannot be empty"): + processor.add_flow_action(flow_config_dict, user, bot) + + def test_add_flow_action_empty_flow_cta(self): + bot = 'test_bot' + user = 'test_user' + action = "test_add_flow_action_empty_name" + processor = MongoProcessor() + flow_config_dict = { + "name": action, + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "BOOKING", + "flow_cta": "", + "response": "Test Response", + } + with pytest.raises(ValidationError, match="flow_cta cannot be empty"): + processor.add_flow_action(flow_config_dict, user, bot) + + def test_add_flow_action_empty_flow_token(self): + bot = 'test_bot' + user = 'test_user' + action = "test_add_flow_action_empty_name" + processor = MongoProcessor() + flow_config_dict = { + "name": action, + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "BOOKING", + "flow_cta": "Sign Up", + "response": "Test Response", + } + with pytest.raises(ValidationError, match="flow_token cannot be empty"): + processor.add_flow_action(flow_config_dict, user, bot) + + def test_add_flow_action(self): + bot = 'test_bot' + user = 'test_user' + action = "flow_action" + processor = MongoProcessor() + flow_config_dict = { + "name": action, + "flow_id": {"value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "recipient_phone": {'value': "919911837465"}, + "initial_screen": "REGISTER", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "data_exchange", + "flow_cta": "Sign Up", + "response": "Test Response", + } + action_id = processor.add_flow_action(flow_config_dict, user, bot) + assert action_id + + def test_add_flow_action_with_name_exists(self): + bot = 'test_bot' + user = 'test_user' + action = "flow_action" + processor = MongoProcessor() + flow_config_dict = { + "name": action, + "flow_id": {"value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "recipient_phone": {'value': "919911837465"}, + "initial_screen": "REGISTER", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "data_exchange", + "flow_cta": "Sign Up", + "response": "Test Response", + } + with pytest.raises(AppException, match="Action exists!"): + processor.add_flow_action(flow_config_dict, user, bot) + + def test_add_flow_action_with_slot_values(self): + bot = 'test_bot' + user = 'test_user' + action = "flow_action_with_slot_values" + processor = MongoProcessor() + flow_config_dict = { + "name": action, + "flow_id": {"parameter_type": "slot", "value": "flow_id"}, + "body": "Fill the Form to Book", + "mode": "draft", + "flow_token": "AHSKSSLLHLSKLSKS", + "flow_action": "navigate", + "recipient_phone": {'value': "phone", "parameter_type": "slot"}, + "initial_screen": "BOOKING", + "flow_cta": "Book Now", + "response": "Flow Triggered", + "dispatch_response": False + } + action_id = processor.add_flow_action(flow_config_dict, user, bot) + assert action_id + + def test_get_flow_actions(self): + bot = 'test_bot' + user = 'test_user' + processor = MongoProcessor() + flow_actions = processor.list_flow_actions(bot) + flow_actions_list = list(flow_actions) + assert len(flow_actions_list) == 2 + + assert flow_actions_list[0]["name"] == "flow_action" + assert flow_actions_list[0]["flow_id"] == {'_cls': 'CustomActionRequestParameters', 'encrypt': False, + 'value': '9191123456789', 'parameter_type': 'value'} + assert flow_actions_list[0]["body"] == "Fill the Sign Up Form" + assert flow_actions_list[0]["mode"] == "published" + assert flow_actions_list[0]["flow_action"] == "data_exchange" + assert flow_actions_list[0]["flow_token"] == "AHSKSSLLHLSKLSKS" + assert flow_actions_list[0]["recipient_phone"] == {'_cls': 'CustomActionRequestParameters', + 'encrypt': False, 'value': '919911837465', + 'parameter_type': 'value'} + assert flow_actions_list[0]["initial_screen"] == "REGISTER" + assert flow_actions_list[0]["flow_cta"] == "Sign Up" + assert flow_actions_list[0]["dispatch_response"] + assert flow_actions_list[0]["response"] == "Test Response" + + assert flow_actions_list[1]["name"] == "flow_action_with_slot_values" + assert flow_actions_list[1]["flow_id"] == {'_cls': 'CustomActionRequestParameters', 'encrypt': False, + 'value': 'flow_id', 'parameter_type': 'slot'} + assert flow_actions_list[1]["body"] == "Fill the Form to Book" + assert flow_actions_list[1]["mode"] == "draft" + assert flow_actions_list[1]["flow_action"] == "navigate" + assert flow_actions_list[1]["flow_token"] == "AHSKSSLLHLSKLSKS" + assert flow_actions_list[1]["recipient_phone"] == {'_cls': 'CustomActionRequestParameters', + 'encrypt': False, 'value': 'phone', + 'parameter_type': 'slot'} + assert flow_actions_list[1]["initial_screen"] == "BOOKING" + assert flow_actions_list[1]["flow_cta"] == "Book Now" + assert not flow_actions_list[1]["dispatch_response"] + assert flow_actions_list[1]["response"] == "Flow Triggered" + + def test_update_flow_action_empty_body(self): + bot = 'test_bot' + user = 'test_user' + action = "flow_action" + processor = MongoProcessor() + flow_config_dict = { + "name": action, + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "", + "mode": "draft", + "flow_token": "AHSKSSLLKAKLLSSLSLSSL", + "flow_action": "navigate", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "REGISTER", + "flow_cta": "Sign Up", + "response": "Test Response", + } + with pytest.raises(ValidationError, match="body cannot be empty"): + processor.update_flow_action(flow_config_dict, user, bot) + + def test_update_flow_action_empty_initial_screen(self): + bot = 'test_bot' + user = 'test_user' + action = "flow_action" + processor = MongoProcessor() + flow_config_dict = { + "name": action, + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "AHSKSSLLKAKLLSSLSLSSL", + "flow_action": "navigate", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "", + "flow_cta": "Sign Up", + "response": "Test Response", + } + with pytest.raises(ValidationError, match="initial_screen cannot be empty"): + processor.update_flow_action(flow_config_dict, user, bot) + + def test_update_flow_action_empty_flow_cta(self): + bot = 'test_bot' + user = 'test_user' + action = "flow_action" + processor = MongoProcessor() + flow_config_dict = { + "name": action, + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "AHSKSSLLKAKLLSSLSLSSL", + "flow_action": "navigate", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "BOOKING", + "flow_cta": "", + "response": "Test Response", + } + with pytest.raises(ValidationError, match="flow_cta cannot be empty"): + processor.update_flow_action(flow_config_dict, user, bot) + + def test_update_flow_action_empty_flow_token(self): + bot = 'test_bot' + user = 'test_user' + action = "flow_action" + processor = MongoProcessor() + flow_config_dict = { + "name": action, + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "BOOKING", + "flow_cta": "Sign Up", + "response": "Test Response", + } + with pytest.raises(ValidationError, match="flow_token cannot be empty"): + processor.update_flow_action(flow_config_dict, user, bot) + + def test_update_flow_action_does_not_exist(self): + bot = 'test_bot' + user = 'test_user' + action = "flow_action_does_not_exist" + processor = MongoProcessor() + flow_config_dict = { + "name": action, + "flow_id": {"parameter_type": "value", "value": "9191123456789"}, + "body": "Fill the Sign Up Form", + "mode": "draft", + "flow_token": "AHSKSSLLKAKLLSSLSLSSL", + "flow_action": "navigate", + "recipient_phone": {'value': "sender_id", "parameter_type": "slot"}, + "initial_screen": "BOOKING", + "flow_cta": "", + "response": "Test Response", + } + with pytest.raises(AppException, match='Action with name "flow_action_does_not_exist" not found'): + processor.update_flow_action(flow_config_dict, user, bot) + + def test_update_flow_action(self): + bot = 'test_bot' + user = 'test_user' + action = "flow_action" + processor = MongoProcessor() + flow_config_dict = { + "name": "flow_action", + "flow_id": {"parameter_type": "slot", "value": "flow_id"}, + "body": "Fill Your Details", + "recipient_phone": {'value': "919913456772"}, + "mode": "draft", + "flow_token": "AHSKSSLLKAKLLSSLSLSSL", + "flow_action": "navigate", + "initial_screen": "DETAILS", + "flow_cta": "Fill Form", + "response": "Form Triggered", + "dispatch_response": False + } + action_id = processor.update_flow_action(flow_config_dict, user, bot) + assert action_id + + def test_get_flow_actions_after_updated(self): + bot = 'test_bot' + user = 'test_user' + processor = MongoProcessor() + flow_actions = processor.list_flow_actions(bot) + flow_actions_list = list(flow_actions) + print(flow_actions_list) + assert len(flow_actions_list) == 2 + + assert flow_actions_list[0]["name"] == "flow_action" + assert flow_actions_list[0]["flow_id"] == {'_cls': 'CustomActionRequestParameters', 'encrypt': False, + 'value': 'flow_id', 'parameter_type': 'slot'} + assert flow_actions_list[0]["body"] == "Fill Your Details" + assert flow_actions_list[0]["mode"] == "draft" + assert flow_actions_list[0]["flow_action"] == "navigate" + assert flow_actions_list[0]["flow_token"] == "AHSKSSLLKAKLLSSLSLSSL" + assert flow_actions_list[0]["recipient_phone"] == {'_cls': 'CustomActionRequestParameters', 'encrypt': False, + 'value': '919913456772', 'parameter_type': 'value'} + assert flow_actions_list[0]["initial_screen"] == "DETAILS" + assert flow_actions_list[0]["flow_cta"] == "Fill Form" + assert not flow_actions_list[0]["dispatch_response"] + assert flow_actions_list[0]["response"] == "Form Triggered" + + assert flow_actions_list[1]["name"] == "flow_action_with_slot_values" + assert flow_actions_list[1]["flow_id"] == {'_cls': 'CustomActionRequestParameters', 'encrypt': False, + 'value': 'flow_id', 'parameter_type': 'slot'} + assert flow_actions_list[1]["body"] == "Fill the Form to Book" + assert flow_actions_list[1]["mode"] == "draft" + assert flow_actions_list[1]["flow_action"] == "navigate" + assert flow_actions_list[1]["flow_token"] == "AHSKSSLLHLSKLSKS" + assert flow_actions_list[1]["recipient_phone"] == {'_cls': 'CustomActionRequestParameters', + 'encrypt': False, 'value': 'phone', + 'parameter_type': 'slot'} + assert flow_actions_list[1]["initial_screen"] == "BOOKING" + assert flow_actions_list[1]["flow_cta"] == "Book Now" + assert not flow_actions_list[1]["dispatch_response"] + assert flow_actions_list[1]["response"] == "Flow Triggered" + + def test_delete_flow_action(self): + name = 'flow_action' + bot = 'test_bot' + user = 'test_user' + processor = MongoProcessor() + processor.delete_action(name, bot, user) + actions = list(processor.list_flow_actions(bot, True)) + assert len(actions) == 1 + + def test_delete_flow_action_already_deleted(self): + name = 'flow_action' + bot = 'test_bot' + user = 'test_user' + processor = MongoProcessor() + with pytest.raises(AppException, match='Action with name "flow_action" not found'): + processor.delete_action(name, bot, user) + def test_add_pyscript_action_empty_name(self): bot = 'test_bot' user = 'test_user' @@ -10923,7 +11308,8 @@ def test_add_complex_story_without_http_action(self): 'email_action': [], 'google_search_action': [], 'jira_action': [], 'zendesk_action': [], 'pipedrive_leads_action': [], 'hubspot_forms_action': [], 'two_stage_fallback': [], 'kairon_bot_response': [], 'razorpay_action': [], 'prompt_action': [], 'actions': [], - 'database_action': [], 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [] + 'database_action': [], 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [], + 'flow_action': [] } def test_add_complex_story_with_action(self): @@ -10946,7 +11332,7 @@ def test_add_complex_story_with_action(self): 'form_validation_action': [], 'email_action': [], 'google_search_action': [], 'jira_action': [], 'zendesk_action': [], 'pipedrive_leads_action': [], 'hubspot_forms_action': [], 'two_stage_fallback': [], 'kairon_bot_response': [], 'razorpay_action': [], 'prompt_action': [], 'database_action': [], - 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [] + 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [], 'flow_action': [] } def test_add_complex_story(self): @@ -10971,7 +11357,8 @@ def test_add_complex_story(self): 'slot_set_action': [], 'email_action': [], 'form_validation_action': [], 'kairon_bot_response': [], 'razorpay_action': [], 'prompt_action': ['gpt_llm_faq'], - 'database_action': [], 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [], + 'database_action': [], 'pyscript_action': [], 'web_search_action': [], + 'live_agent_action': [], 'flow_action': [], 'utterances': ['utter_greet', 'utter_cheer_up', 'utter_did_that_help', @@ -12744,7 +13131,7 @@ def test_list_actions(self): 'http_action': ['action_performanceuser1000@digite.com'], 'zendesk_action': [], 'slot_set_action': [], 'hubspot_forms_action': [], 'two_stage_fallback': [], 'kairon_bot_response': [], 'razorpay_action': [], 'email_action': [], 'form_validation_action': [], 'prompt_action': [], 'database_action': [], - 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [], + 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [], 'flow_action': [], 'utterances': ['utter_offer_help', 'utter_query', 'utter_goodbye', 'utter_feedback', 'utter_default', 'utter_please_rephrase'], 'web_search_action': []}, ignore_order=True) @@ -12854,6 +13241,7 @@ def test_add_rule(self): 'razorpay_action': [], 'prompt_action': ['gpt_llm_faq'], 'slot_set_action': [], 'email_action': [], 'form_validation_action': [], 'jira_action': [], 'database_action': [], 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [], + 'flow_action': [], 'utterances': ['utter_greet', 'utter_cheer_up', 'utter_did_that_help', From d1c0ed20148c2c0a6d9d97e23803b10742222f2b Mon Sep 17 00:00:00 2001 From: Mahesh Date: Tue, 4 Jun 2024 16:30:44 +0530 Subject: [PATCH 2/2] 1. Added code for the Flow Action 2. added APIs along with unit and integration test cases for the same. --- kairon/actions/definitions/flow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kairon/actions/definitions/flow.py b/kairon/actions/definitions/flow.py index 039d91ac3..bb6f98ef0 100644 --- a/kairon/actions/definitions/flow.py +++ b/kairon/actions/definitions/flow.py @@ -65,6 +65,8 @@ async def execute(self, dispatcher: CollectingDispatcher, tracker: Tracker, doma headers=headers, http_url=http_url, request_method="POST", request_body=body ) logger.info("response: " + str(http_response)) + logger.info("status_code: " + str(status_code)) + logger.info("time_elapsed: " + str(time_elapsed)) except Exception as e: exception = str(e) logger.exception(e)