diff --git a/kairon/actions/definitions/factory.py b/kairon/actions/definitions/factory.py index 27549b8b7..8cee637f4 100644 --- a/kairon/actions/definitions/factory.py +++ b/kairon/actions/definitions/factory.py @@ -7,6 +7,7 @@ from kairon.actions.definitions.http import ActionHTTP from kairon.actions.definitions.hubspot import ActionHubspotForms from kairon.actions.definitions.jira import ActionJiraTicket +from kairon.actions.definitions.live_agent import ActionLiveAgent from kairon.actions.definitions.pipedrive import ActionPipedriveLeads from kairon.actions.definitions.prompt import ActionPrompt from kairon.actions.definitions.pyscript import ActionPyscript @@ -39,7 +40,8 @@ class ActionFactory: ActionType.prompt_action.value: ActionPrompt, ActionType.pyscript_action.value: ActionPyscript, ActionType.database_action.value: ActionDatabase, - ActionType.web_search_action.value: ActionWebSearch + ActionType.web_search_action.value: ActionWebSearch, + ActionType.live_agent_action.value: ActionLiveAgent } @staticmethod diff --git a/kairon/actions/definitions/live_agent.py b/kairon/actions/definitions/live_agent.py new file mode 100644 index 000000000..de4220932 --- /dev/null +++ b/kairon/actions/definitions/live_agent.py @@ -0,0 +1,110 @@ +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 ActionServerLogs, LiveAgentActionConfig +from kairon.shared.actions.exception import ActionFailure +from kairon.shared.actions.models import ActionType, DispatchType +from kairon.shared.actions.utils import ActionUtility +from kairon.shared.live_agent.live_agent import LiveAgentHandler +from kairon.shared.constants import ChannelTypes + + +CONST_CHANNEL_NAME_MAP = { + 'TelegramHandler': ChannelTypes.TELEGRAM.value, + 'facebook': ChannelTypes.MESSENGER.value, + 'instagram': ChannelTypes.INSTAGRAM.value, + 'whatsapp': ChannelTypes.WHATSAPP.value, +} + + +class ActionLiveAgent(ActionsBase): + + def __init__(self, bot: Text, name: Text): + """ + Initialize HTTP action. + + @param bot: bot id + @param name: action name + """ + self.bot = bot + self.name = 'live_agent_action' + self.__response = None + self.__is_success = False + + def retrieve_config(self): + """ + Fetch LiveAgentAction configuration parameters from the database + + :return: HttpActionConfig containing configuration for the action as a dict. + """ + try: + live_agent_config_dict = LiveAgentActionConfig.objects().get(bot=self.bot, + name=self.name, status=True).to_mongo().to_dict() + logger.debug("live_agent_action_config: " + str(live_agent_config_dict)) + return live_agent_config_dict + except DoesNotExist as e: + logger.exception(e) + raise ActionFailure("No Live Agent 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 + exception = None + filled_slots = {} + dispatch_bot_response = True + status = "SUCCESS" + msg_logger = [] + + try: + action_config = self.retrieve_config() + dispatch_bot_response = action_config.get('dispatch_bot_response', True) + bot_response = action_config.get('bot_response') + channel = CONST_CHANNEL_NAME_MAP[tracker.get_latest_input_channel()] + await LiveAgentHandler.request_live_agent(self.bot, tracker.sender_id, channel) + self.__is_success = True + self.__response = bot_response + except Exception as e: + exception = e + self.__is_success = False + logger.exception(e) + status = "FAILURE" + bot_response = bot_response if bot_response else "Sorry, I am unable to process your request at the moment." + finally: + if dispatch_bot_response: + bot_response, message = ActionUtility.handle_utter_bot_response(dispatcher, DispatchType.text.value, bot_response) + if message: + msg_logger.append(message) + ActionServerLogs( + type=ActionType.live_agent_action.value, + intent=tracker.get_intent_of_latest_message(skip_fallback_intent=False), + action=self.name, + sender=tracker.sender_id, + bot_response=str(bot_response) if bot_response else None, + messages=msg_logger, + exception=str(exception) if exception else None, + bot=self.bot, + status=status, + user_msg=tracker.latest_message.get('text') + ).save() + return filled_slots + + @property + def is_success(self): + return self.__is_success + + @property + def response(self): + return self.__response diff --git a/kairon/api/app/routers/bot/action.py b/kairon/api/app/routers/bot/action.py index 5baad3b2b..d0aa2e63d 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 + WebSearchActionRequest, LiveAgentActionRequest ) from kairon.shared.constants import TESTER_ACCESS, DESIGNER_ACCESS from kairon.shared.models import User @@ -559,3 +559,46 @@ async def update_razorpay_action( """ mongo_processor.edit_razorpay_action(request_data.dict(), current_user.get_bot(), current_user.get_user()) return Response(message="Action updated!") + + +@router.get("/live_agent", response_model=Response) +async def get_live_agent( + current_user: User = Security(Authentication.get_current_user_and_bot, scopes=DESIGNER_ACCESS) +): + """ + Returns configuration for live agent action. + """ + config = mongo_processor.get_live_agent(current_user.get_bot()) + return Response(data=config) + + +@router.post("/live_agent", response_model=Response) +async def enable_live_agent( + request_data: LiveAgentActionRequest, + current_user: User = Security(Authentication.get_current_user_and_bot, scopes=DESIGNER_ACCESS) +): + status = mongo_processor.enable_live_agent(request_data.dict(), current_user.get_bot(), current_user.get_user()) + msg = "Live Agent Action enabled!" if status else "Live Agent Action already enabled!" + return Response(message=msg) + + +@router.put("/live_agent", response_model=Response) +async def update_live_agent( + request_data: LiveAgentActionRequest, + current_user: User = Security(Authentication.get_current_user_and_bot, scopes=DESIGNER_ACCESS) +): + """ + Updates the live agent action config. + """ + + mongo_processor.edit_live_agent(request_data.dict(), current_user.get_bot(), current_user.get_user()) + return Response(message="Action updated!") + + +@router.get("/live_agent/disable", response_model=Response) +async def disable_live_agent( + current_user: User = Security(Authentication.get_current_user_and_bot, scopes=DESIGNER_ACCESS) +): + mongo_processor.disable_live_agent(current_user.get_bot()) + return Response(message="Live Agent Action disabled!") + diff --git a/kairon/api/app/routers/bot/bot.py b/kairon/api/app/routers/bot/bot.py index edd36d15b..3a09b25ce 100644 --- a/kairon/api/app/routers/bot/bot.py +++ b/kairon/api/app/routers/bot/bot.py @@ -26,7 +26,7 @@ from kairon.shared.actions.data_objects import ActionServerLogs from kairon.shared.auth import Authentication from kairon.shared.constants import TESTER_ACCESS, DESIGNER_ACCESS, CHAT_ACCESS, UserActivityType, ADMIN_ACCESS, \ - VIEW_ACCESS, EventClass + VIEW_ACCESS, EventClass, AGENT_ACCESS from kairon.shared.data.assets_processor import AssetsProcessor from kairon.shared.data.audit.processor import AuditDataProcessor from kairon.shared.data.constant import EVENT_STATUS, ENDPOINT_TYPE, TOKEN_TYPE, ModelTestType, \ @@ -41,6 +41,8 @@ from kairon.shared.models import User, TemplateType from kairon.shared.test.processor import ModelTestingLogProcessor from kairon.shared.utils import Utility +from kairon.shared.live_agent.live_agent import LiveAgentHandler + router = APIRouter() v2 = APIRouter() @@ -1655,3 +1657,13 @@ async def update_bot_settings( """Updates bot settings""" MongoProcessor.edit_bot_settings(bot_settings.dict(), current_user.get_bot(), current_user.get_user()) return Response(message='Bot Settings updated') + + +@router.get("/live_agent_token", response_model=Response) +async def get_live_agent_token(current_user: User = Security(Authentication.get_current_user_and_bot, scopes=AGENT_ACCESS)): + """ + Fetches existing list of stories (conversation flows) + """ + data = await LiveAgentHandler.authenticate_agent(current_user.get_user(), current_user.get_bot()) + return Response(data=data) + diff --git a/kairon/api/models.py b/kairon/api/models.py index 07bb35c04..edb47ec67 100644 --- a/kairon/api/models.py +++ b/kairon/api/models.py @@ -472,6 +472,11 @@ def validate_collection_name(cls, v, values, **kwargs): return v +class LiveAgentActionRequest(BaseModel): + bot_response: str = "connecting to live agent" + dispatch_bot_response: bool = True + + class TrainingData(BaseModel): intent: constr(to_lower=True, strip_whitespace=True) training_examples: List[str] diff --git a/kairon/chat/agent_processor.py b/kairon/chat/agent_processor.py index 32a98d80a..a88f27614 100644 --- a/kairon/chat/agent_processor.py +++ b/kairon/chat/agent_processor.py @@ -3,12 +3,14 @@ from loguru import logger as logging from rasa.core.agent import Agent +from rasa.core.channels import UserMessage from kairon.shared.chat.cache.in_memory_agent import AgentCache from kairon.exceptions import AppException from kairon.shared.data.processor import MongoProcessor from .agent.agent import KaironAgent from kairon.shared.chat.cache.in_memory_agent import InMemoryAgentCache +from ..shared.live_agent.live_agent import LiveAgentHandler from ..shared.utils import Utility from kairon.shared.otel import record_custom_attributes @@ -32,7 +34,6 @@ def get_agent(bot: Text) -> Agent: """ if not AgentProcessor.cache_provider.is_exists(bot) or not AgentProcessor.is_latest_version_in_mem(bot): AgentProcessor.reload(bot) - record_custom_attributes(num_models=AgentProcessor.cache_provider.len()) return AgentProcessor.cache_provider.get(bot) @@ -69,3 +70,11 @@ def is_latest_version_in_mem(bot: Text): in_mem_model_ver = AgentProcessor.cache_provider.get(bot).model_ver logging.debug(f"PID:{os.getpid()} In memory model:{in_mem_model_ver}, latest trained model:{latest_ver}") return latest_ver == in_mem_model_ver + + @staticmethod + async def handle_channel_message(bot: Text, userdata: UserMessage): + is_live_agent_enabled = await LiveAgentHandler.check_live_agent_active(bot, userdata) + logging.debug(f"Live agent enabled:{is_live_agent_enabled}") + if not is_live_agent_enabled: + return await AgentProcessor.get_agent(bot).handle_message(userdata) + return await LiveAgentHandler.process_live_agent(bot, userdata) diff --git a/kairon/chat/handlers/channels/messenger.py b/kairon/chat/handlers/channels/messenger.py index 50a4bee06..e5a590431 100644 --- a/kairon/chat/handlers/channels/messenger.py +++ b/kairon/chat/handlers/channels/messenger.py @@ -199,7 +199,8 @@ async def _handle_user_message( @staticmethod async def process_message(bot: str, user_message: UserMessage): - await AgentProcessor.get_agent(bot).handle_message(user_message) + await AgentProcessor.handle_channel_message(bot, user_message) + #await AgentProcessor.get_agent(bot).handle_message(user_message) class MessengerBot(OutputChannel): diff --git a/kairon/chat/handlers/channels/msteams.py b/kairon/chat/handlers/channels/msteams.py index 5bc63d0f3..ee6315055 100644 --- a/kairon/chat/handlers/channels/msteams.py +++ b/kairon/chat/handlers/channels/msteams.py @@ -298,7 +298,7 @@ def is_validate_hash(request: Request): jwt_token = Utility.decrypt_message(secrettoken) return secrethash == token, jwt_token - async def validate(self) : + async def validate(self): return {"status": "ok"} async def handle_message(self): diff --git a/kairon/chat/handlers/channels/telegram.py b/kairon/chat/handlers/channels/telegram.py index 382b8a14b..bace014d2 100644 --- a/kairon/chat/handlers/channels/telegram.py +++ b/kairon/chat/handlers/channels/telegram.py @@ -260,7 +260,7 @@ async def handle_message(self): @staticmethod async def process_message(bot: str, user_message: UserMessage): - await AgentProcessor.get_agent(bot).handle_message(user_message) + await AgentProcessor.handle_channel_message(bot, user_message) @staticmethod def get_output_channel(access_token, webhook_url) -> TelegramOutput: diff --git a/kairon/chat/handlers/channels/whatsapp.py b/kairon/chat/handlers/channels/whatsapp.py index e3e524865..1f2a373d1 100644 --- a/kairon/chat/handlers/channels/whatsapp.py +++ b/kairon/chat/handlers/channels/whatsapp.py @@ -125,7 +125,7 @@ async def _handle_user_message( @staticmethod async def process_message(bot: str, user_message: UserMessage): - await AgentProcessor.get_agent(bot).handle_message(user_message) + await AgentProcessor.handle_channel_message(bot, user_message) def __get_access_token(self): provider = self.config.get("bsp_type", "meta") diff --git a/kairon/chat/utils.py b/kairon/chat/utils.py index 268a4482b..159650ce5 100644 --- a/kairon/chat/utils.py +++ b/kairon/chat/utils.py @@ -282,6 +282,12 @@ def __get_metadata( metadata.update(default_metadata) return metadata + + + + + + @staticmethod def add_telemetry_metadata(x_telemetry_uid: Text, x_telemetry_sid: Text, metadata: Dict = None): if not metadata: diff --git a/kairon/shared/actions/data_objects.py b/kairon/shared/actions/data_objects.py index 5c1b8be92..54f433762 100644 --- a/kairon/shared/actions/data_objects.py +++ b/kairon/shared/actions/data_objects.py @@ -854,3 +854,30 @@ def clean(self): signals.pre_save_post_validation.connect( HttpActionConfig.pre_save_post_validation, sender=HttpActionConfig ) + + +@auditlogger.log +@push_notification.apply +class LiveAgentActionConfig(Auditlog): + name = StringField(default='live_agent_action') + bot_response = StringField(default='Connecting to live agent') + dispatch_bot_response = BooleanField(default=True) + bot = StringField(required=True) + user = StringField(required=True) + timestamp = DateTimeField(default=datetime.utcnow) + status = BooleanField(default=True) + + meta = {"indexes": [{"fields": ["bot", ("bot", "action_name", "status")]}]} + + def validate(self, clean=True): + if clean: + self.clean() + + def clean(self): + self.name = self.name.strip().lower() + if Utility.check_empty_string(self.name): + raise ValidationError("Action name cannot be empty or blank spaces") + if self.name.startswith("utter_"): + raise ValidationError("Action name cannot start with utter_") + + diff --git a/kairon/shared/actions/models.py b/kairon/shared/actions/models.py index 71e20fef7..51e212976 100644 --- a/kairon/shared/actions/models.py +++ b/kairon/shared/actions/models.py @@ -47,6 +47,7 @@ class ActionType(str, Enum): pyscript_action = "pyscript_action" database_action = "database_action" web_search_action = "web_search_action" + live_agent_action = "live_agent_action" class HttpRequestContentType(str, Enum): diff --git a/kairon/shared/chat/processor.py b/kairon/shared/chat/processor.py index 3ffb2d1d2..9fb16bc15 100644 --- a/kairon/shared/chat/processor.py +++ b/kairon/shared/chat/processor.py @@ -31,7 +31,8 @@ def save_channel_config(configuration: Dict, bot: Text, user: Text): filter_args = ChatDataProcessor.__attach_metadata_and_get_filter(configuration, bot) channel = Channels.objects(**filter_args).get() channel.config = configuration['config'] - primary_slack_config_changed = True if channel.connector_type == 'slack' and channel.config.get('is_primary') else False + primary_slack_config_changed = True if channel.connector_type == 'slack' and channel.config.get( + 'is_primary') else False except DoesNotExist: channel = Channels(**configuration) channel.bot = bot @@ -55,6 +56,9 @@ def __attach_metadata_and_get_filter(configuration: Dict, bot: Text): filter_args["config__team__id"] = configuration['config']['team']['id'] return filter_args + def __getattribute__(self, __name): + return super().__getattribute__(__name) + @staticmethod def delete_channel_config(bot: Text, **kwargs): """ diff --git a/kairon/shared/constants.py b/kairon/shared/constants.py index e207bf8b4..a0cc5ba14 100644 --- a/kairon/shared/constants.py +++ b/kairon/shared/constants.py @@ -22,6 +22,9 @@ VIEW_ACCESS = [ACCESS_ROLES.OWNER.value, ACCESS_ROLES.ADMIN.value, ACCESS_ROLES.DESIGNER.value, ACCESS_ROLES.TESTER.value, ACCESS_ROLES.CHAT.value, ACCESS_ROLES.VIEW.value] +AGENT_ACCESS = [ACCESS_ROLES.OWNER.value, ACCESS_ROLES.ADMIN.value, ACCESS_ROLES.DESIGNER.value, ACCESS_ROLES.TESTER.value, ACCESS_ROLES.CHAT.value, ACCESS_ROLES.VIEW.value, ACCESS_ROLES.AGENT.value] + + KAIRON_USER_MSG_ENTITY = "kairon_user_msg" FAQ_DISABLED_ERR = "Faq feature is disabled for the bot! Please contact support." diff --git a/kairon/shared/data/constant.py b/kairon/shared/data/constant.py index 19e0c8b3c..a94319920 100644 --- a/kairon/shared/data/constant.py +++ b/kairon/shared/data/constant.py @@ -142,6 +142,7 @@ class ACCESS_ROLES(str, Enum): TESTER = "tester" CHAT = "chat" VIEW = "view" + AGENT = "agent" class ACTIVITY_STATUS(str, Enum): diff --git a/kairon/shared/data/processor.py b/kairon/shared/data/processor.py index 0ef70de18..3ca39287b 100644 --- a/kairon/shared/data/processor.py +++ b/kairon/shared/data/processor.py @@ -79,7 +79,7 @@ DbQuery, PyscriptActionConfig, WebSearchAction, - UserQuestion, + UserQuestion, LiveAgentActionConfig, ) from kairon.shared.actions.models import ( ActionType, @@ -158,6 +158,7 @@ from ..constants import KaironSystemSlots, PluginTypes, EventClass from ..custom_widgets.data_objects import CustomWidgets from ..importer.data_objects import ValidationLogs +from ..live_agent.live_agent import LiveAgentHandler from ..multilingual.data_objects import BotReplicationLogs from ..test.data_objects import ModelTestingLogs @@ -1253,8 +1254,8 @@ def __prepare_autofill(self, mappings: list, slot_name: str): new_mappings = mappings.copy() for mapping in new_mappings: if ( - mapping.get(MAPPING_TYPE) == SlotMappingType.FROM_ENTITY.value - and mapping.get("entity") == slot_name + mapping.get(MAPPING_TYPE) == SlotMappingType.FROM_ENTITY.value + and mapping.get("entity") == slot_name ): auto_fill = True break @@ -1440,6 +1441,11 @@ def __retrieve_existing_components(self, bot): StoryStepType.web_search_action.value: dict( WebSearchAction.objects(bot=bot, status=True).values_list("name", "id") ), + StoryStepType.two_stage_fallback_action.value: dict( + LiveAgentActionConfig.objects(bot=bot, status=True).values_list( + "name", "id" + ) + ), } return component_dict @@ -3842,9 +3848,6 @@ def update_db_action(self, request_data: Dict, user: str, bot: str): :param bot: bot id :return: VectorDb configuration id for updated VectorDb action config """ - bot_settings = MongoProcessor.get_bot_settings(bot=bot, user=user) - if not bot_settings['llm_settings']["enable_faq"]: - raise AppException("Faq feature is disabled for the bot! Please contact support.") if not Utility.is_exist( DatabaseAction, @@ -3884,9 +3887,6 @@ def add_db_action(self, vector_db_action_config: Dict, user: str, bot: str): :param bot: bot id :return: Http configuration id for saved Http action config """ - bot_settings = MongoProcessor.get_bot_settings(bot=bot, user=user) - if not bot_settings['llm_settings']["enable_faq"]: - raise AppException("Faq feature is disabled for the bot! Please contact support.") self.__validate_payload(vector_db_action_config.get("payload"), bot) Utility.is_valid_action_name( vector_db_action_config.get("name"), bot, DatabaseAction @@ -3901,7 +3901,7 @@ def add_db_action(self, vector_db_action_config: Dict, user: str, bot: str): action_id = ( DatabaseAction( name=vector_db_action_config["name"], - collection=vector_db_action_config['collection'], + collection=vector_db_action_config['collection'], query_type=vector_db_action_config.get("query_type"), payload=DbQuery(**vector_db_action_config.get("payload")), response=HttpActionResponse( @@ -6008,7 +6008,7 @@ def add_or_update_slot_mapping(self, mapping: dict, bot: Text, user: Text): """ try: if not Utility.is_exist( - Slots, raise_error=False, name=mapping["slot"], bot=bot, status=True + Slots, raise_error=False, name=mapping["slot"], bot=bot, status=True ): raise AppException(f'Slot with name \'{mapping["slot"]}\' not found') slot_mapping = SlotMapping.objects( @@ -6033,7 +6033,7 @@ def add_slot_mapping(self, mapping: dict, bot: Text, user: Text): :return: document id of the mapping """ if not Utility.is_exist( - Slots, raise_error=False, name=mapping["slot"], bot=bot, status=True + Slots, raise_error=False, name=mapping["slot"], bot=bot, status=True ): raise AppException(f'Slot with name "{mapping["slot"]}" not found') form_name = None @@ -6051,7 +6051,7 @@ def add_slot_mapping(self, mapping: dict, bot: Text, user: Text): mapping=mapping["mapping"], bot=bot, user=user, - form_name= form_name, + form_name=form_name, ) return slot_mapping.save().id.__str__() @@ -6073,8 +6073,6 @@ def update_slot_mapping(self, mapping: dict, slot_mapping_id: str): except Exception as e: raise AppException(e) - - def delete_single_slot_mapping(self, slot_mapping_id: str): """ Delete slot mapping. @@ -6087,10 +6085,6 @@ def delete_single_slot_mapping(self, slot_mapping_id: str): except Exception as e: raise AppException(e) - - - - def __prepare_slot_mappings(self, bot: Text): """ Fetches existing slot mappings. @@ -6102,7 +6096,7 @@ def __prepare_slot_mappings(self, bot: Text): for mapping in mappings: yield {mapping["slot"]: mapping["mapping"]} - def get_slot_mappings(self, bot: Text, form: Text = None, include_id = False): + def get_slot_mappings(self, bot: Text, form: Text = None, include_id=False): """ Fetches existing slot mappings. @@ -7580,3 +7574,56 @@ def get_razorpay_action_config(self, bot: Text, with_doc_id: bool = True): action.pop("user") yield action + + def get_live_agent(self, bot: Text): + try: + live_agent = LiveAgentActionConfig.objects(bot=bot, status=True).get() + live_agent = live_agent.to_mongo().to_dict() + live_agent.pop("_id") + live_agent.pop("bot") + live_agent.pop("user") + live_agent.pop("status") + live_agent.pop("timestamp") + return live_agent + except: + return [] + + def enable_live_agent(self, request_data: dict, bot: Text, user: Text): + action_name = "live_agent_action" + enabled = False + if not Utility.is_exist( + Actions, + name__iexact=action_name, + type=ActionType.live_agent_action.value, + bot=bot, + status=True, + raise_error=False + ): + self.add_action( + action_name, + bot, + user, + raise_exception=False, + action_type=ActionType.live_agent_action.value, + ) + enabled = True + if not Utility.is_exist(LiveAgentActionConfig, raise_error=False, bot=bot, user=user): + live_agent = LiveAgentActionConfig(**request_data, bot=bot, user=user, status=True) + live_agent.save() + + return enabled + + def edit_live_agent(self, request_data: dict, bot: Text, user: Text): + live_agent = LiveAgentActionConfig.objects(bot=bot, user=user).update( + set__bot_response=request_data.get('bot_response'), + set__dispatch_bot_response=request_data.get('dispatch_bot_response') + ) + if not live_agent: + raise AppException("Live agent not enabled for the bot") + + def disable_live_agent(self, bot: Text): + Utility.hard_delete_document([Actions], bot, name__iexact="live_agent_action") + Utility.hard_delete_document([LiveAgentActionConfig], bot=bot) + + def is_live_agent_enabled(self, bot: Text): + return Utility.is_exist(LiveAgentActionConfig, raise_error=False, bot=bot, status=True) \ No newline at end of file diff --git a/kairon/shared/live_agent/live_agent.py b/kairon/shared/live_agent/live_agent.py new file mode 100644 index 000000000..b2d448770 --- /dev/null +++ b/kairon/shared/live_agent/live_agent.py @@ -0,0 +1,96 @@ +import logging +from kairon.shared.actions.utils import ActionUtility +from rasa.core.channels import UserMessage + +from kairon import Utility + +logger = logging.getLogger(__name__) + + +class LiveAgentHandler: + + @staticmethod + async def request_live_agent(bot_id: str, sender_id: str, channel: str): + url = f"{Utility.environment['live_agent']['url']}/conversation/request" + auth_token = Utility.environment['live_agent']['auth_token'] + + data = { + "bot_id": bot_id, + "sender_id": sender_id, + "channel": channel, + } + headers = { + 'Content-Type': 'application/json', + 'Authorization': f"Bearer {auth_token}" + } + res, status, _ = await ActionUtility.execute_request_async(url, 'POST', data, headers) + if status != 200: + raise Exception(res.get('message', "Failed to process request")) + return res.get('data') + + + @staticmethod + async def close_conversation(identifier): + url = f"{Utility.environment['live_agent']['url']}/conversation/close/{identifier}" + headers = { + 'Content-Type': 'application/json' + } + res, status, _ = await ActionUtility.execute_request_async(url, 'GET', None, headers) + if status != 200: + raise Exception(res.get('message', "Failed to process request")) + return res.get('data') + + @staticmethod + async def process_live_agent(bot_id, userdata: UserMessage): + text = userdata.text + if text is None or text.strip() == "": + return False + url = f"{Utility.environment['live_agent']['url']}/conversation/chat" + auth_token = Utility.environment['live_agent']['auth_token'] + headers = { + 'Content-Type': 'application/json', + 'Authorization': f"Bearer {auth_token}" + } + data = { + "bot_id": bot_id, + "sender_id": userdata.sender_id, + "channel": userdata.output_channel.name(), + "message": userdata.text + } + + res, status, _ = await ActionUtility.execute_request_async(url, 'POST', data, headers) + if status != 200: + raise Exception(res.get('message', "Failed to process request")) + + @staticmethod + async def check_live_agent_active(bot_id, userdata: UserMessage): + channel = userdata.output_channel.name() + sender_id = userdata.sender_id + url = f"{Utility.environment['live_agent']['url']}/conversation/status/{bot_id}/{channel}/{sender_id}" + auth_token = Utility.environment['live_agent']['auth_token'] + headers = { + 'Content-Type': 'application/json', + 'Authorization': f"Bearer {auth_token}" + } + res, status, _ = await ActionUtility.execute_request_async(url, 'GET', None, headers) + if status != 200: + raise Exception(res.get('message', "Failed to process request")) + return res['data']['status'] + + @staticmethod + async def authenticate_agent(user, bot_id): + url = f"{Utility.environment['live_agent']['url']}/auth" + data = { + "bot_id": bot_id, + "user": user + } + auth_token = Utility.environment['live_agent']['auth_token'] + headers = { + 'Content-Type': 'application/json', + 'Authorization': f"Bearer {auth_token}" + } + res, status, _ = await ActionUtility.execute_request_async(url, 'POST', data, headers) + logger.info(res) + if status != 200: + raise Exception(res.get('message', "Failed to process request")) + return res.get('data') diff --git a/kairon/shared/models.py b/kairon/shared/models.py index d5221ea5a..c5f437103 100644 --- a/kairon/shared/models.py +++ b/kairon/shared/models.py @@ -27,6 +27,7 @@ class StoryStepType(str, Enum): prompt_action = "PROMPT_ACTION" database_action = "DATABASE_ACTION" web_search_action = "WEB_SEARCH_ACTION" + live_agent_action = "LIVE_AGENT_ACTION" class StoryType(str, Enum): diff --git a/kairon/shared/trackers.py b/kairon/shared/trackers.py index f76a04208..ef97cf0d9 100644 --- a/kairon/shared/trackers.py +++ b/kairon/shared/trackers.py @@ -243,6 +243,7 @@ def get_latest_session_events_count(self, sender_id: Text): filter_query["event.timestamp"] = { "$gte": last_session[0]["event"]["timestamp"] } + filter_query["event.metadata.type"] = {"$exists": False} stored = list( self.conversations.aggregate( @@ -262,8 +263,9 @@ def get_latest_session_events_count(self, sender_id: Text): def _additional_events(self, tracker: DialogueStateTracker) -> Iterator: count = self.get_latest_session_events_count(tracker.sender_id) total_events = len(tracker.events) + logger.debug(tracker.events) logger.debug(f"tracker existing events : {count}") logger.debug(f"tracker total events : {total_events}") - if count: + if count and count < total_events: return itertools.islice(tracker.events, count, total_events) return tracker.events diff --git a/metadata/integrations.yml b/metadata/integrations.yml index fe925b98b..98cd14170 100644 --- a/metadata/integrations.yml +++ b/metadata/integrations.yml @@ -53,6 +53,7 @@ channels: - app_secret - access_token - verify_token + - phone_number business_providers: 360dialog: required_fields: diff --git a/metadata/roles.yml b/metadata/roles.yml index bb0a9029e..97a89ffd1 100644 --- a/metadata/roles.yml +++ b/metadata/roles.yml @@ -20,6 +20,7 @@ roles: manageQnaStories: true broadcast: true customAnalytics: true + liveAgent: true pages: chatbots: renameBot: true @@ -108,6 +109,8 @@ roles: list: true delete: true viewLogs: true + liveAgent: + chatView: true admin: menu: @@ -130,6 +133,7 @@ roles: manageQnaStories: true broadcast: true customAnalytics: true + liveAgent: true pages: chatbots: renameBot: false @@ -218,6 +222,8 @@ roles: list: true delete: true viewLogs: true + liveAgent: + chatView: true designer: menu: @@ -240,6 +246,7 @@ roles: manageQnaStories: true broadcast: true customAnalytics: true + liveAgent: false pages: chatbots: renameBot: false @@ -328,6 +335,8 @@ roles: list: true delete: true viewLogs: true + liveAgent: + chatView: false tester: menu: @@ -350,6 +359,7 @@ roles: manageQnaStories: true broadcast: false customAnalytics: true + liveAgent: false pages: chatbots: renameBot: false @@ -438,6 +448,123 @@ roles: list: false delete: false viewLogs: true + liveAgent: + chatView: false + + agent: + menu: + chatbots: true + common: false + conversations: true + components: true + advancedConfigurations: false + trainingHistory: true + modelConfiguration: true + configureClient: true + configureTelemetry: false + configureNudge: false + integrations: false + administration: false + chatHistory: true + analytics: true + logs: true + generateStories: false + manageQnaStories: true + broadcast: false + customAnalytics: true + liveAgent: true + pages: + chatbots: + renameBot: false + deleteBot: false + common: + trainBot: false + testBot: true + conversations: + add: false + update: false + view: true + delete: false + components: + add: false + update: false + view: true + delete: false + advancedConfigurations: + uploadFiles: false + fileDownload: false + modelDownload: false + dataValidationLogs: true + trainingHistory: + reloadModel: true + testModel: false + viewModelTrainingLogs: true + viewModelTestingLogs: true + modelTestingResults: true + modelConfiguration: + applyTemplate: false + configureClient: + save: false + get: true + generate: false + configureTelemetry: + save: false + get: true + configureNudge: + save: false + get: true + integrations: + get: false + save: false + getEndpoint: false + administration: + addIntegrationToken: false + updateIntegrationToken: false + revokeIntegrationToken: false + getIntegrationToken: false + addActionServerUrl: false + updateActionServerUrl: false + addHistoryServerUrl: false + updateHistoryServerUrl: false + addMember: false + updateMember: false + getMember: true + deleteMember: false + transferOwnership: false + addSecret: false + updateSecret: false + getSecret: false + deleteSecret: false + addCustomWidget: false + deleteCustomWidget: false + updateCustomWidget: false + chatHistory: + viewHistory: true + deleteHistory: false + downloadHistory: false + viewDeletionLogs: false + dataValidationLogs: + viewLogs: true + validateExistingData: false + modelTestingLogs: + viewLogs: true + testModel: false + manageQnaStories: + add: false + edit: false + delete: false + augment: false + download: false + broadcast: + add: false + edit: false + list: false + delete: false + viewLogs: true + liveAgent: + chatView: true + + view: menu: @@ -460,6 +587,7 @@ roles: manageQnaStories: false broadcast: false customAnalytics: true + liveAgent: false pages: chatbots: renameBot: false @@ -548,3 +676,5 @@ roles: list: false delete: false viewLogs: false + liveAgent: + chatView: false diff --git a/system.yaml b/system.yaml index 0bbf03aaf..bab32539c 100644 --- a/system.yaml +++ b/system.yaml @@ -242,5 +242,6 @@ core: - SpacyEntityExtractor - SpacyFeaturizer - - +live_agent: + url: ${LIVE_AGENT_SERVER_URL:"http://localhost:8000/api/v1"} + auth_token: ${LIVE_AGENT_AUTH_TOKEN:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia2Fpcm9uIn0.EJXuG2n8dy72h9J-8SoIBToVmnB4gpXDGKg_Smcz-C8"} diff --git a/tests/integration_test/action_service_test.py b/tests/integration_test/action_service_test.py index 064576fa9..19d7e0b1a 100644 --- a/tests/integration_test/action_service_test.py +++ b/tests/integration_test/action_service_test.py @@ -19,7 +19,7 @@ EmailActionConfig, ActionServerLogs, GoogleSearchAction, JiraAction, ZendeskAction, PipedriveLeadsAction, SetSlots, \ HubspotFormsAction, HttpActionResponse, HttpActionRequestBody, SetSlotsFromResponse, CustomActionRequestParameters, \ KaironTwoStageFallbackAction, TwoStageFallbackTextualRecommendations, RazorpayAction, PromptAction, FormSlotSet, \ - DatabaseAction, DbQuery, PyscriptActionConfig, WebSearchAction, UserQuestion + DatabaseAction, DbQuery, PyscriptActionConfig, WebSearchAction, UserQuestion, LiveAgentActionConfig from kairon.shared.actions.models import ActionType, ActionParameterType, DispatchType, DbActionOperationType, \ DbQueryValueType from kairon.shared.actions.utils import ActionUtility @@ -55,6 +55,227 @@ def test_index(): assert result["message"] == "Kairon Action Server Up and Running" +def test_live_agent_action_execution(aioresponses): + action_name = "live_agent_action" + Actions(name=action_name, type=ActionType.live_agent_action.value, + bot="5f50fd0a56b698ca10d35d2z", user="user").save() + LiveAgentActionConfig( + name="live_agent_action", + bot_response="Connecting to live agent", + dispatch_bot_response=True, + bot="5f50fd0a56b698ca10d35d2z", + user="user" + ).save() + + aioresponses.add( + method="POST", + url=f"{Utility.environment['live_agent']['url']}/conversation/request", + payload={"success": True, "data": "live agent connected", "message": None, "error_code": 0}, + body={'bot_id': '5f50fd0a56b698ca10d35d2z', 'sender_id': 'default', 'channel': 'messenger'}, + 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': 'live_agent_action'}]}, + "latest_event_time": 1537645578.314389, + "followup_action": "action_listen", + "paused": False, + "events": [ + {"event": "action", "timestamp": 1594907100.12764, "name": "action_session_start", "policy": None, + "confidence": None}, {"event": "session_started", "timestamp": 1594907100.12765}, + {"event": "action", "timestamp": 1594907100.12767, "name": "action_listen", "policy": None, + "confidence": None}, {"event": "user", "timestamp": 1594907100.42744, "text": "can't", + "parse_data": { + "intent": {"name": "test intent", "confidence": 0.253578245639801}, + "entities": [], "intent_ranking": [ + {"name": "test intent", "confidence": 0.253578245639801}, + {"name": "goodbye", "confidence": 0.1504897326231}, + {"name": "greet", "confidence": 0.138640150427818}, + {"name": "affirm", "confidence": 0.0857767835259438}, + {"name": "smalltalk_human", "confidence": 0.0721133947372437}, + {"name": "deny", "confidence": 0.069614589214325}, + {"name": "bot_challenge", "confidence": 0.0664894133806229}, + {"name": "faq_vaccine", "confidence": 0.062177762389183}, + {"name": "faq_testing", "confidence": 0.0530692934989929}, + {"name": "out_of_scope", "confidence": 0.0480506233870983}], + "response_selector": { + "default": {"response": {"name": None, "confidence": 0}, + "ranking": [], "full_retrieval_intent": None}}, + "text": "can't"}, "input_channel": "facebook", + "message_id": "bbd413bf5c834bf3b98e0da2373553b2", "metadata": {}}, + {"event": "action", "timestamp": 1594907100.4308, "name": "utter_test intent", + "policy": "policy_0_MemoizationPolicy", "confidence": 1}, + {"event": "bot", "timestamp": 1594907100.4308, "text": "will not = won\"t", + "data": {"elements": None, "quick_replies": None, "buttons": None, "attachment": None, + "image": None, "custom": None}, "metadata": {}}, + {"event": "action", "timestamp": 1594907100.43384, "name": "action_listen", + "policy": "policy_0_MemoizationPolicy", "confidence": 1}, + {"event": "user", "timestamp": 1594907117.04194, "text": "can\"t", + "parse_data": {"intent": {"name": "test intent", "confidence": 0.253578245639801}, "entities": [], + "intent_ranking": [{"name": "test intent", "confidence": 0.253578245639801}, + {"name": "goodbye", "confidence": 0.1504897326231}, + {"name": "greet", "confidence": 0.138640150427818}, + {"name": "affirm", "confidence": 0.0857767835259438}, + {"name": "smalltalk_human", "confidence": 0.0721133947372437}, + {"name": "deny", "confidence": 0.069614589214325}, + {"name": "bot_challenge", "confidence": 0.0664894133806229}, + {"name": "faq_vaccine", "confidence": 0.062177762389183}, + {"name": "faq_testing", "confidence": 0.0530692934989929}, + {"name": "out_of_scope", "confidence": 0.0480506233870983}], + "response_selector": { + "default": {"response": {"name": None, "confidence": 0}, "ranking": [], + "full_retrieval_intent": None}}, "text": "can\"t"}, + "input_channel": "facebook", "message_id": "e96e2a85de0748798748385503c65fb3", "metadata": {}}, + {"event": "action", "timestamp": 1594907117.04547, "name": "utter_test intent", + "policy": "policy_1_TEDPolicy", "confidence": 0.978452920913696}, + {"event": "bot", "timestamp": 1594907117.04548, "text": "can not = can't", + "data": {"elements": None, "quick_replies": None, "buttons": None, "attachment": None, + "image": None, "custom": None}, "metadata": {}}], + "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() + print(response_json) + assert response.status_code == 200 + assert len(response_json['responses']) == 1 + assert response_json['responses'][0]['text'] == 'Connecting to live agent' + log = ActionServerLogs.objects(action="live_agent_action").get().to_mongo().to_dict() + log.pop('_id') + log.pop('timestamp') + print(log) + assert log == {'type': 'live_agent_action', 'intent': 'live_agent_action', 'action': 'live_agent_action', + 'sender': 'default', 'headers': {}, 'bot_response': 'Connecting to live agent', 'messages': [], + 'bot': '5f50fd0a56b698ca10d35d2z', 'status': 'SUCCESS', 'user_msg': 'get intents'} + + +def test_live_agent_action_execution_with_exception(aioresponses): + action_name = "test_live_agent_action_execution_with_exception" + Actions(name=action_name, type=ActionType.live_agent_action.value, + bot="5f50fd0a56b698ca10d35d21", user="user").save() + LiveAgentActionConfig( + name="live_agent_action", + bot_response="Connecting to live agent", + dispatch_bot_response=True, + bot="5f50fd0a56b698ca10d35d21", + user="user" + ).save() + + aioresponses.add( + method="POST", + url=f"{Utility.environment['live_agent']['url']}/conversation/request", + payload={"success": False, "data": None, "message": "invalid request body", "error_code": 422}, + body={'bot_id': '5f50fd0a56b698ca10d35d2z', 'sender_id': 'default', 'channel': 'invalid'}, + status=400 + ) + + request_object = { + "next_action": action_name, + "tracker": { + "sender_id": "default", + "conversation_id": "default", + "slots": {"bot": "5f50fd0a56b698ca10d35d21", "location": "Bangalore", "langauge": "Kannada"}, + "latest_message": {'text': 'get intents', 'intent_ranking': [{'name': 'live_agent_action'}]}, + "latest_event_time": 1537645578.314389, + "followup_action": "action_listen", + "paused": False, + "events": [ + {"event": "action", "timestamp": 1594907100.12764, "name": "action_session_start", "policy": None, + "confidence": None}, {"event": "session_started", "timestamp": 1594907100.12765}, + {"event": "action", "timestamp": 1594907100.12767, "name": "action_listen", "policy": None, + "confidence": None}, {"event": "user", "timestamp": 1594907100.42744, "text": "can't", + "parse_data": { + "intent": {"name": "test intent", "confidence": 0.253578245639801}, + "entities": [], "intent_ranking": [ + {"name": "test intent", "confidence": 0.253578245639801}, + {"name": "goodbye", "confidence": 0.1504897326231}, + {"name": "greet", "confidence": 0.138640150427818}, + {"name": "affirm", "confidence": 0.0857767835259438}, + {"name": "smalltalk_human", "confidence": 0.0721133947372437}, + {"name": "deny", "confidence": 0.069614589214325}, + {"name": "bot_challenge", "confidence": 0.0664894133806229}, + {"name": "faq_vaccine", "confidence": 0.062177762389183}, + {"name": "faq_testing", "confidence": 0.0530692934989929}, + {"name": "out_of_scope", "confidence": 0.0480506233870983}], + "response_selector": { + "default": {"response": {"name": None, "confidence": 0}, + "ranking": [], "full_retrieval_intent": None}}, + "text": "can't"}, "input_channel": "facebook", + "message_id": "bbd413bf5c834bf3b98e0da2373553b2", "metadata": {}}, + {"event": "action", "timestamp": 1594907100.4308, "name": "utter_test intent", + "policy": "policy_0_MemoizationPolicy", "confidence": 1}, + {"event": "bot", "timestamp": 1594907100.4308, "text": "will not = won\"t", + "data": {"elements": None, "quick_replies": None, "buttons": None, "attachment": None, + "image": None, "custom": None}, "metadata": {}}, + {"event": "action", "timestamp": 1594907100.43384, "name": "action_listen", + "policy": "policy_0_MemoizationPolicy", "confidence": 1}, + {"event": "user", "timestamp": 1594907117.04194, "text": "can\"t", + "parse_data": {"intent": {"name": "test intent", "confidence": 0.253578245639801}, "entities": [], + "intent_ranking": [{"name": "test intent", "confidence": 0.253578245639801}, + {"name": "goodbye", "confidence": 0.1504897326231}, + {"name": "greet", "confidence": 0.138640150427818}, + {"name": "affirm", "confidence": 0.0857767835259438}, + {"name": "smalltalk_human", "confidence": 0.0721133947372437}, + {"name": "deny", "confidence": 0.069614589214325}, + {"name": "bot_challenge", "confidence": 0.0664894133806229}, + {"name": "faq_vaccine", "confidence": 0.062177762389183}, + {"name": "faq_testing", "confidence": 0.0530692934989929}, + {"name": "out_of_scope", "confidence": 0.0480506233870983}], + "response_selector": { + "default": {"response": {"name": None, "confidence": 0}, "ranking": [], + "full_retrieval_intent": None}}, "text": "can\"t"}, + "input_channel": "facebook", "message_id": "e96e2a85de0748798748385503c65fb3", "metadata": {}}, + {"event": "action", "timestamp": 1594907117.04547, "name": "utter_test intent", + "policy": "policy_1_TEDPolicy", "confidence": 0.978452920913696}, + {"event": "bot", "timestamp": 1594907117.04548, "text": "can not = can't", + "data": {"elements": None, "quick_replies": None, "buttons": None, "attachment": None, + "image": None, "custom": None}, "metadata": {}}], + "latest_input_channel": "rest", + "active_loop": {}, + "latest_action": {}, + }, + "domain": { + "config": {}, + "session_config": {}, + "intents": [], + "entities": [], + "slots": {"bot": "5f50fd0a56b698ca10d35d21"}, + "responses": {}, + "actions": [], + "forms": {}, + "e2e_actions": [] + }, + "version": "version" + } + response = client.post("/webhook", json=request_object) + response_json = response.json() + print(response_json) + assert response.status_code == 200 + assert len(response_json['responses']) == 1 + assert response_json['responses'][0]['text'] == 'Connecting to live agent' + 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_pyscript_action_execution(): import textwrap @@ -82,22 +303,14 @@ def test_pyscript_action_execution(): json={"success": True, "data": {"bot_response": {'numbers': [1, 2, 3, 4, 5], 'total': 15, 'i': 5}, "slots": {"location": "Bangalore", "langauge": "Kannada"}, "type": "json"}, "message": None, "error_code": 0}, - match=[responses.matchers.json_params_matcher({'source_code': script, - 'predefined_objects': {'chat_log': [], - 'intent': 'pyscript_action', - 'kairon_user_msg': None, 'key_vault': {}, - 'latest_message': {'intent_ranking': [ - {'name': 'pyscript_action'}], - 'text': 'get intents'}, - 'sender_id': 'default', - 'session_started': None, - 'slot': { - 'bot': '5f50fd0a56b698ca10d35d2z', - 'langauge': 'Kannada', - 'location': 'Bangalore'}, - 'user_message': 'get intents'} - - })] + match=[responses.matchers.json_params_matcher( {'source_code': script, + 'predefined_objects': {'chat_log': [], 'intent': 'pyscript_action', + 'kairon_user_msg': None, 'key_vault': {}, 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], 'text': 'get intents'}, + 'sender_id': 'default', 'session_started': None, + 'slot': {'bot': '5f50fd0a56b698ca10d35d2z', 'langauge': 'Kannada', 'location': 'Bangalore'}, + 'user_message': 'get intents'} + + })] ) request_object = { @@ -223,7 +436,6 @@ def test_pyscript_action_execution_with_multiple_utterances(): assert response_json['responses'][0]['custom'] == {'text': 'Hello!'} assert response_json['responses'][1]['text'] == 'How can I help you?' - @responses.activate def test_pyscript_action_execution_with_multiple_integer_utterances(): import textwrap @@ -337,9 +549,7 @@ def test_pyscript_action_execution_with_bot_response_none(): "message": None, "error_code": 0}, match=[responses.matchers.json_params_matcher( {'source_code': script, - 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', - 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], - 'text': 'get intents'}, + 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], 'text': 'get intents'}, 'slot': {'bot': '5f50fd0a56b698ca10d35d2z', 'location': 'Bangalore', 'langauge': 'Kannada'}, 'intent': 'pyscript_action', 'chat_log': [], 'key_vault': {}, 'kairon_user_msg': None, 'session_started': None}})] @@ -414,9 +624,7 @@ def test_pyscript_action_execution_with_type_json_bot_response_none(): "message": None, "error_code": 0}, match=[responses.matchers.json_params_matcher( {'source_code': script, - 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', - 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], - 'text': 'get intents'}, + 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], 'text': 'get intents'}, 'slot': {'bot': '5f50fd0a56b698ca10d35d2z', 'location': 'Bangalore', 'langauge': 'Kannada'}, 'intent': 'pyscript_action', 'chat_log': [], 'key_vault': {}, 'kairon_user_msg': None, 'session_started': None}})] @@ -491,9 +699,7 @@ def test_pyscript_action_execution_with_type_json_bot_response_str(): "message": None, "error_code": 0}, match=[responses.matchers.json_params_matcher( {'source_code': script, - 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', - 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], - 'text': 'get intents'}, + 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], 'text': 'get intents'}, 'slot': {'bot': '5f50fd0a56b698ca10d35d2z', 'location': 'Bangalore', 'langauge': 'Kannada'}, 'intent': 'pyscript_action', 'chat_log': [], 'key_vault': {}, 'kairon_user_msg': None, 'session_started': None}})] @@ -570,9 +776,7 @@ def test_pyscript_action_execution_with_other_type(): "message": None, "error_code": 0}, match=[responses.matchers.json_params_matcher( {'source_code': script, - 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', - 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], - 'text': 'get intents'}, + 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], 'text': 'get intents'}, 'slot': {'bot': '5f50fd0a56b698ca10d35d2z', 'location': 'Bangalore', 'langauge': 'Kannada'}, 'intent': 'pyscript_action', 'chat_log': [], 'key_vault': {}, 'kairon_user_msg': None, 'session_started': None}})] @@ -647,9 +851,7 @@ def test_pyscript_action_execution_with_slots_not_dict_type(): "slots": "invalid slots values"}, "message": None, "error_code": 0}, match=[responses.matchers.json_params_matcher( {'source_code': script, - 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', - 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], - 'text': 'get intents'}, + 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], 'text': 'get intents'}, 'slot': {'bot': '5f50fd0a56b698ca10d35d2z', 'location': 'Bangalore', 'langauge': 'Kannada'}, 'intent': 'pyscript_action', 'chat_log': [], 'key_vault': {}, 'kairon_user_msg': None, 'session_started': None}})] @@ -754,17 +956,15 @@ def test_pyscript_action_execution_without_pyscript_evaluator_url(mock_trigger_l assert len(response_json['events']) == 3 assert len(response_json['responses']) == 1 assert response_json['events'] == [ - {'event': 'slot', 'timestamp': None, 'name': 'location', 'value': 'Bangalore'}, - {'event': 'slot', 'timestamp': None, 'name': 'langauge', 'value': 'Kannada'}, - {'event': 'slot', 'timestamp': None, 'name': 'kairon_action_response', - 'value': "Successfully Evaluated the pyscript"}] + {'event': 'slot', 'timestamp': None, 'name': 'location', 'value': 'Bangalore'}, + {'event': 'slot', 'timestamp': None, 'name': 'langauge', 'value': 'Kannada'}, + {'event': 'slot', 'timestamp': None, 'name': 'kairon_action_response', + 'value': "Successfully Evaluated the pyscript"}] assert response_json['responses'][0]['text'] == "Successfully Evaluated the pyscript" called_args = mock_trigger_lambda.call_args assert called_args.args[1] == \ {'source_code': script, - 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', - 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], - 'text': 'get intents'}, + 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], 'text': 'get intents'}, 'slot': {"bot": "5f50fd0a56b698ca10d35d2z", "location": "Bangalore", "langauge": "Kannada"}, 'intent': 'pyscript_action', 'chat_log': [], 'key_vault': {}, @@ -831,8 +1031,8 @@ def test_pyscript_action_execution_without_pyscript_evaluator_url_raise_exceptio 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': "I have failed to process your request"}] + {'event': 'slot', 'timestamp': None, 'name': 'kairon_action_response', + 'value': "I have failed to process your request"}] log = ActionServerLogs.objects(action=action_name).get().to_mongo().to_dict() assert log['exception'] == "Failed to evaluated the pyscript" @@ -867,9 +1067,7 @@ def raise_custom_exception(request): "POST", Utility.environment['evaluator']['pyscript']['url'], callback=raise_custom_exception, match=[responses.matchers.json_params_matcher( {'source_code': script, - 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', - 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], - 'text': 'get intents'}, + 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], 'text': 'get intents'}, 'slot': {'bot': '5f50fd0a56b698ca10d35d2z', 'location': 'Bangalore', 'langauge': 'Kannada'}, 'intent': 'pyscript_action', 'chat_log': [], 'key_vault': {}, 'kairon_user_msg': None, 'session_started': None}})] @@ -880,7 +1078,7 @@ def raise_custom_exception(request): "tracker": { "sender_id": "default", "conversation_id": "default", - "slots": {"bot": "5f50fd0a56b698ca10d35d2z", "location": "Bangalore", "langauge": "Kannada"}, + "slots": {"bot": "5f50fd0a56b698ca10d35d2z", "location": "Bangalore", "langauge": "Kannada"}, "latest_message": {'text': 'get intents', 'intent_ranking': [{'name': 'pyscript_action'}]}, "latest_event_time": 1537645578.314389, "followup_action": "action_listen", @@ -940,9 +1138,7 @@ def test_pyscript_action_execution_with_invalid_response(): "error_code": 422}, match=[responses.matchers.json_params_matcher( {'source_code': script, - 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', - 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], - 'text': 'get intents'}, + 'predefined_objects': {'sender_id': 'default', 'user_message': 'get intents', 'latest_message': {'intent_ranking': [{'name': 'pyscript_action'}], 'text': 'get intents'}, 'slot': {'bot': '5f50fd0a56b698ca10d35d2z', 'location': 'Bangalore', 'langauge': 'Kannada'}, 'intent': 'pyscript_action', 'chat_log': [], 'key_vault': {}, 'kairon_user_msg': None, 'session_started': None}})] @@ -984,8 +1180,7 @@ def test_pyscript_action_execution_with_invalid_response(): {'event': 'slot', 'timestamp': None, 'name': 'kairon_action_response', 'value': 'I have failed to process your request'}] log = ActionServerLogs.objects(action=action_name).get().to_mongo().to_dict() - assert log[ - 'exception'] == 'Pyscript evaluation failed: {\'success\': False, \'data\': None, \'message\': \'Script execution error: ("Line 2: SyntaxError: invalid syntax at statement: for i in 10",)\', \'error_code\': 422}' + assert log['exception'] == 'Pyscript evaluation failed: {\'success\': False, \'data\': None, \'message\': \'Script execution error: ("Line 2: SyntaxError: invalid syntax at statement: for i in 10",)\', \'error_code\': 422}' def test_http_action_execution(aioresponses): @@ -2713,7 +2908,7 @@ def test_http_action_execution_script_evaluation_failure_and_dispatch_2(aiorespo }) aioresponses.add( method=responses.GET, - url=http_url + "?" + urlencode({"bot": "5f50fd0a56b698ca10d35d2e", "user": "1011", "tag": "from_bot"}), + url=f"{http_url}?bot=5f50fd0a56b698ca10d35d2e&user=1011&tag=from_bot", body=resp_msg, status=200 ) @@ -2774,7 +2969,7 @@ def test_http_action_execution_script_evaluation_failure_and_dispatch_2(aiorespo assert response_json['events'] == [ {"event": "slot", "timestamp": None, "name": "kairon_action_response", "value": "I have failed to process your request"}, - {"event": "slot", "timestamp": None, "name": "http_status_code", "value": 200}, ] + {"event": "slot", "timestamp": None, "name": "http_status_code", "value": 200},] assert response_json['responses'][0]['text'] == "I have failed to process your request" diff --git a/tests/integration_test/chat_service_test.py b/tests/integration_test/chat_service_test.py index e5531aa98..bd2ec577c 100644 --- a/tests/integration_test/chat_service_test.py +++ b/tests/integration_test/chat_service_test.py @@ -5,6 +5,8 @@ from datetime import datetime, timedelta from unittest import mock from urllib.parse import urlencode, quote_plus + +from kairon.shared.live_agent.live_agent import LiveAgentHandler from kairon.shared.utils import Utility os.environ["system_file"] = "./tests/testing_data/system.yaml" @@ -160,6 +162,7 @@ "app_secret": "jagbd34567890", "access_token": "ERTYUIEFDGHGFHJKLFGHJKGHJ", "verify_token": "valid", + "phone_number": "1234567890", }, }, bot, @@ -1783,66 +1786,73 @@ def _mock_validate_hub_signature(*args, **kwargs): actual = response.json() assert actual == "not validated" +@pytest.mark.asyncio +async def _mock_check_live_agent_active(*args, **kwargs): + return False + @responses.activate def test_whatsapp_valid_text_message_request(): - def _mock_validate_hub_signature(*args, **kwargs): + async def _mock_validate_hub_signature(*args, **kwargs): return True responses.add("POST", "https://graph.facebook.com/v13.0/12345678/messages", json={}) - with patch.object( - MessengerHandler, "validate_hub_signature", _mock_validate_hub_signature - ): - response = client.post( - f"/api/bot/whatsapp/{bot}/{token}", - headers={"hub.verify_token": "valid"}, - json={ - "object": "whatsapp_business_account", - "entry": [ - { - "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", - "changes": [ - { - "value": { - "messaging_product": "whatsapp", - "metadata": { - "display_phone_number": "910123456789", - "phone_number_id": "12345678", + + with patch.object(LiveAgentHandler, "check_live_agent_active", _mock_check_live_agent_active): + + with patch.object( + MessengerHandler, "validate_hub_signature", _mock_validate_hub_signature + ): + response = client.post( + f"/api/bot/whatsapp/{bot}/{token}", + headers={"hub.verify_token": "valid"}, + json={ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "910123456789", + "phone_number_id": "12345678", + }, + "contacts": [ + { + "profile": {"name": "udit"}, + "wa_id": "wa-123456789", + } + ], + "messages": [ + { + "from": "910123456789", + "id": "wappmsg.ID", + "timestamp": "21-09-2022 12:05:00", + "text": {"body": "hi"}, + "type": "text", + } + ], }, - "contacts": [ - { - "profile": {"name": "udit"}, - "wa_id": "wa-123456789", - } - ], - "messages": [ - { - "from": "910123456789", - "id": "wappmsg.ID", - "timestamp": "21-09-2022 12:05:00", - "text": {"body": "hi"}, - "type": "text", - } - ], - }, - "field": "messages", - } - ], - } - ], - }, - ) - time.sleep(10) + "field": "messages", + } + ], + } + ], + }, + ) + time.sleep(10) - actual = response.json() - assert actual == "success" - assert ( - MeteringProcessor.get_metric_count( - user["account"], metric_type=MetricType.prod_chat, channel_type="whatsapp" + actual = response.json() + assert actual == "success" + assert ( + MeteringProcessor.get_metric_count( + user["account"], metric_type=MetricType.prod_chat, channel_type="whatsapp" + ) + > 0 ) - > 0 - ) @responses.activate @@ -3610,43 +3620,45 @@ def _mock_validate_hub_signature(*args, **kwargs): "POST", f"https://graph.facebook.com/v2.12/me/messages?access_token={access_token}", json={} ) - with patch.object(InstagramHandler, "validate_hub_signature", _mock_validate_hub_signature): - response = client.post( - f"/api/bot/instagram/{bot}/{token}", - headers={"hub.verify_token": "valid"}, - json={ - "entry": [ - { - "id": "17841456706109718", - "time": 1707144192, - "changes": [ - { - "value": { - "from": { - "id": "6489091794524304", - "username": "kairon_user_123" - }, - "media": { - "id": "18013303267972611", - "media_product_type": "REELS" + + with patch.object(LiveAgentHandler, "check_live_agent_active", _mock_check_live_agent_active): + with patch.object(InstagramHandler, "validate_hub_signature", _mock_validate_hub_signature): + response = client.post( + f"/api/bot/instagram/{bot}/{token}", + headers={"hub.verify_token": "valid"}, + json={ + "entry": [ + { + "id": "17841456706109718", + "time": 1707144192, + "changes": [ + { + "value": { + "from": { + "id": "6489091794524304", + "username": "kairon_user_123" + }, + "media": { + "id": "18013303267972611", + "media_product_type": "REELS" + }, + "id": "18009764417219041", + "text": "Hi" }, - "id": "18009764417219041", - "text": "Hi" - }, - "field": "comments" - } - ] - } - ], - "object": "instagram" - }) - time.sleep(5) + "field": "comments" + } + ] + } + ], + "object": "instagram" + }) + time.sleep(5) - actual = response.json() - print(f"Actual response for instagram is {actual}") - assert actual == 'success' - assert MeteringProcessor.get_metric_count(user['account'], metric_type=MetricType.prod_chat, - channel_type="instagram") > 0 + actual = response.json() + print(f"Actual response for instagram is {actual}") + assert actual == 'success' + assert MeteringProcessor.get_metric_count(user['account'], metric_type=MetricType.prod_chat, + channel_type="instagram") > 0 @responses.activate @@ -3654,43 +3666,53 @@ def test_instagram_comment_with_parent_comment(): def _mock_validate_hub_signature(*args, **kwargs): return True - with patch.object(InstagramHandler, "validate_hub_signature", _mock_validate_hub_signature): - response = client.post( - f"/api/bot/instagram/{bot}/{token}", - headers={"hub.verify_token": "valid"}, - json={ - "entry": [ - { - "id": "17841456706109718", - "time": 1707144192, - "changes": [ - { - "value": { - "from": { - "id": "6489091794524304", - "username": "_hdg_photography" - }, - "media": { - "id": "18013303267972611", - "media_product_type": "REELS" + + with patch.object(LiveAgentHandler, "check_live_agent_active", _mock_check_live_agent_active): + with patch.object(InstagramHandler, "validate_hub_signature", _mock_validate_hub_signature): + responses.add( + "GET", + json={"data": {"status": False}}, + url=f"{Utility.environment['live_agent']['url']}/conversation/status/*" + + ) + + response = client.post( + f"/api/bot/instagram/{bot}/{token}", + headers={"hub.verify_token": "valid"}, + json={ + "entry": [ + { + "id": "17841456706109718", + "time": 1707144192, + "changes": [ + { + "value": { + "from": { + "id": "6489091794524304", + "username": "_hdg_photography" + }, + "media": { + "id": "18013303267972611", + "media_product_type": "REELS" + }, + "id": "18009764417219042", + "parent_id": "18009764417219041", + "text": "Hi" }, - "id": "18009764417219042", - "parent_id": "18009764417219041", - "text": "Hi" - }, - "field": "comments" - } - ] - } - ], - "object": "instagram" - }) + "field": "comments" + } + ] + } + ], + "object": "instagram" + }) - actual = response.json() - print(f"Actual response for instagram is {actual}") - assert actual == 'success' - assert MeteringProcessor.get_metric_count(user['account'], metric_type=MetricType.prod_chat, - channel_type="instagram") > 0 + + actual = response.json() + print(f"Actual response for instagram is {actual}") + assert actual == 'success' + assert MeteringProcessor.get_metric_count(user['account'], metric_type=MetricType.prod_chat, + channel_type="instagram") > 0 def test_chat_when_botownerchanged(): diff --git a/tests/integration_test/services_test.py b/tests/integration_test/services_test.py index 452557e82..35bfa672d 100644 --- a/tests/integration_test/services_test.py +++ b/tests/integration_test/services_test.py @@ -1123,6 +1123,147 @@ def test_list_bots(): assert response["data"]["account_owned"][1]["_id"] assert response["data"]["shared"] == [] +def test_get_live_agent_with_no_live_agent(): + response = client.get( + url=f"/api/bot/{pytest.bot}/action/live_agent", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["data"] == [] + assert actual["error_code"] == 0 + assert not actual["message"] + assert actual["success"] + + +def test_enable_live_agent(): + request_body = { + "bot_response": "connecting to live agent", + "dispatch_bot_response": False, + } + + response = client.post( + url=f"/api/bot/{pytest.bot}/action/live_agent", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 0 + assert actual["message"] == 'Live Agent Action enabled!' + assert actual["success"] + + +def test_get_live_agent_after_enabled(): + response = client.get( + url=f"/api/bot/{pytest.bot}/action/live_agent", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + print(actual) + assert actual["data"] == {'name': 'live_agent_action', + 'bot_response': 'connecting to live agent', + 'dispatch_bot_response': False} + assert actual["error_code"] == 0 + assert not actual["message"] + assert actual["success"] + + +def test_enable_live_agent_already_exist(): + request_body = { + "bot_response": "connecting to live agent", + "dispatch_bot_response": False, + } + + response = client.post( + url=f"/api/bot/{pytest.bot}/action/live_agent", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 0 + assert actual["message"] == 'Live Agent Action already enabled!' + assert actual["success"] + + +def test_update_live_agent(): + request_body = { + "bot_response": "connecting to different live agent...", + "dispatch_bot_response": True, + } + + response = client.put( + url=f"/api/bot/{pytest.bot}/action/live_agent", + 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_live_agent_after_updated(): + response = client.get( + url=f"/api/bot/{pytest.bot}/action/live_agent", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + print(actual) + assert actual["data"] == {'name': 'live_agent_action', + 'bot_response': 'connecting to different live agent...', + 'dispatch_bot_response': True} + assert actual["error_code"] == 0 + assert not actual["message"] + assert actual["success"] + + +def test_disable_live_agent(): + response = client.get( + url=f"/api/bot/{pytest.bot}/action/live_agent/disable", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert not actual["data"] + assert actual["error_code"] == 0 + assert actual["message"] == "Live Agent Action disabled!" + assert actual["success"] + + +def test_update_live_agent_does_not_exist(): + request_body = { + "bot_response": "connecting to different live agent...", + "dispatch_bot_response": True, + } + + response = client.put( + url=f"/api/bot/{pytest.bot}/action/live_agent", + json=request_body, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == 'Live agent not enabled for the bot' + assert not actual["success"] + + +def test_get_live_agent_after_disabled(): + response = client.get( + url=f"/api/bot/{pytest.bot}/action/live_agent", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert actual["data"] == [] + assert actual["error_code"] == 0 + assert not actual["message"] + assert actual["success"] + def test_add_pyscript_action_empty_name(): script = """ @@ -5668,10 +5809,11 @@ def test_add_story_invalid_event_type(): "PROMPT_ACTION", "DATABASE_ACTION", "WEB_SEARCH_ACTION", + "LIVE_AGENT_ACTION" ] }, "loc": ["body", "steps", 0, "type"], - "msg": "value is not a valid enumeration member; permitted: 'INTENT', 'SLOT', 'FORM_START', 'FORM_END', 'BOT', 'HTTP_ACTION', 'ACTION', 'SLOT_SET_ACTION', 'FORM_ACTION', 'GOOGLE_SEARCH_ACTION', 'EMAIL_ACTION', 'JIRA_ACTION', 'ZENDESK_ACTION', 'PIPEDRIVE_LEADS_ACTION', 'HUBSPOT_FORMS_ACTION', 'RAZORPAY_ACTION', 'TWO_STAGE_FALLBACK_ACTION', 'PYSCRIPT_ACTION', 'PROMPT_ACTION', 'DATABASE_ACTION', 'WEB_SEARCH_ACTION'", + "msg": "value is not a valid enumeration member; permitted: 'INTENT', 'SLOT', 'FORM_START', 'FORM_END', 'BOT', 'HTTP_ACTION', 'ACTION', 'SLOT_SET_ACTION', 'FORM_ACTION', 'GOOGLE_SEARCH_ACTION', 'EMAIL_ACTION', 'JIRA_ACTION', 'ZENDESK_ACTION', 'PIPEDRIVE_LEADS_ACTION', 'HUBSPOT_FORMS_ACTION', 'RAZORPAY_ACTION', 'TWO_STAGE_FALLBACK_ACTION', 'PYSCRIPT_ACTION', 'PROMPT_ACTION', 'DATABASE_ACTION', 'WEB_SEARCH_ACTION', 'LIVE_AGENT_ACTION'", "type": "type_error.enum", } ] @@ -6313,7 +6455,7 @@ def test_add_multiflow_story_invalid_event_type(): "'BOT', 'HTTP_ACTION', 'ACTION', 'SLOT_SET_ACTION', 'FORM_ACTION', 'GOOGLE_SEARCH_ACTION', " "'EMAIL_ACTION', 'JIRA_ACTION', 'ZENDESK_ACTION', 'PIPEDRIVE_LEADS_ACTION', " "'HUBSPOT_FORMS_ACTION', 'RAZORPAY_ACTION', 'TWO_STAGE_FALLBACK_ACTION', 'PYSCRIPT_ACTION', " - "'PROMPT_ACTION', 'DATABASE_ACTION', 'WEB_SEARCH_ACTION'", + "'PROMPT_ACTION', 'DATABASE_ACTION', 'WEB_SEARCH_ACTION', 'LIVE_AGENT_ACTION'", "type": "type_error.enum", "ctx": { "enum_values": [ @@ -6338,6 +6480,7 @@ def test_add_multiflow_story_invalid_event_type(): "PROMPT_ACTION", "DATABASE_ACTION", "WEB_SEARCH_ACTION", + "LIVE_AGENT_ACTION" ] }, } @@ -6428,10 +6571,11 @@ def test_update_story_invalid_event_type(): "PROMPT_ACTION", "DATABASE_ACTION", "WEB_SEARCH_ACTION", + "LIVE_AGENT_ACTION" ] }, "loc": ["body", "steps", 0, "type"], - "msg": "value is not a valid enumeration member; permitted: 'INTENT', 'SLOT', 'FORM_START', 'FORM_END', 'BOT', 'HTTP_ACTION', 'ACTION', 'SLOT_SET_ACTION', 'FORM_ACTION', 'GOOGLE_SEARCH_ACTION', 'EMAIL_ACTION', 'JIRA_ACTION', 'ZENDESK_ACTION', 'PIPEDRIVE_LEADS_ACTION', 'HUBSPOT_FORMS_ACTION', 'RAZORPAY_ACTION', 'TWO_STAGE_FALLBACK_ACTION', 'PYSCRIPT_ACTION', 'PROMPT_ACTION', 'DATABASE_ACTION', 'WEB_SEARCH_ACTION'", + "msg": "value is not a valid enumeration member; permitted: 'INTENT', 'SLOT', 'FORM_START', 'FORM_END', 'BOT', 'HTTP_ACTION', 'ACTION', 'SLOT_SET_ACTION', 'FORM_ACTION', 'GOOGLE_SEARCH_ACTION', 'EMAIL_ACTION', 'JIRA_ACTION', 'ZENDESK_ACTION', 'PIPEDRIVE_LEADS_ACTION', 'HUBSPOT_FORMS_ACTION', 'RAZORPAY_ACTION', 'TWO_STAGE_FALLBACK_ACTION', 'PYSCRIPT_ACTION', 'PROMPT_ACTION', 'DATABASE_ACTION', 'WEB_SEARCH_ACTION', 'LIVE_AGENT_ACTION'", "type": "type_error.enum", } ] @@ -6788,7 +6932,7 @@ def test_update_multiflow_story_invalid_event_type(): "'FORM_END', 'BOT', 'HTTP_ACTION', 'ACTION', 'SLOT_SET_ACTION', 'FORM_ACTION', " "'GOOGLE_SEARCH_ACTION', 'EMAIL_ACTION', 'JIRA_ACTION', 'ZENDESK_ACTION', " "'PIPEDRIVE_LEADS_ACTION', 'HUBSPOT_FORMS_ACTION', 'RAZORPAY_ACTION', " - "'TWO_STAGE_FALLBACK_ACTION', 'PYSCRIPT_ACTION', 'PROMPT_ACTION', 'DATABASE_ACTION', 'WEB_SEARCH_ACTION'", + "'TWO_STAGE_FALLBACK_ACTION', 'PYSCRIPT_ACTION', 'PROMPT_ACTION', 'DATABASE_ACTION', 'WEB_SEARCH_ACTION', 'LIVE_AGENT_ACTION'", "type": "type_error.enum", "ctx": { "enum_values": [ @@ -6813,6 +6957,7 @@ def test_update_multiflow_story_invalid_event_type(): "PROMPT_ACTION", "DATABASE_ACTION", "WEB_SEARCH_ACTION", + "LIVE_AGENT_ACTION" ] }, } @@ -9754,7 +9899,7 @@ def test_add_vectordb_action_without_enable_faq(): actual = response.json() assert actual["error_code"] == 422 - assert actual["message"] == 'Faq feature is disabled for the bot! Please contact support.' + assert actual["message"] == 'Collection does not exist!' assert not actual["success"] @@ -11284,7 +11429,7 @@ def test_list_actions(): 'hubspot_forms_action': [], 'two_stage_fallback': [], 'kairon_bot_response': [], 'razorpay_action': [], 'prompt_action': [], - 'pyscript_action': [], 'web_search_action': []} + 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': []} assert actual["success"] @@ -12168,10 +12313,11 @@ def test_add_rule_invalid_event_type(): "PROMPT_ACTION", "DATABASE_ACTION", "WEB_SEARCH_ACTION", + "LIVE_AGENT_ACTION", ] }, "loc": ["body", "steps", 0, "type"], - "msg": "value is not a valid enumeration member; permitted: 'INTENT', 'SLOT', 'FORM_START', 'FORM_END', 'BOT', 'HTTP_ACTION', 'ACTION', 'SLOT_SET_ACTION', 'FORM_ACTION', 'GOOGLE_SEARCH_ACTION', 'EMAIL_ACTION', 'JIRA_ACTION', 'ZENDESK_ACTION', 'PIPEDRIVE_LEADS_ACTION', 'HUBSPOT_FORMS_ACTION', 'RAZORPAY_ACTION', 'TWO_STAGE_FALLBACK_ACTION', 'PYSCRIPT_ACTION', 'PROMPT_ACTION', 'DATABASE_ACTION', 'WEB_SEARCH_ACTION'", + "msg": "value is not a valid enumeration member; permitted: 'INTENT', 'SLOT', 'FORM_START', 'FORM_END', 'BOT', 'HTTP_ACTION', 'ACTION', 'SLOT_SET_ACTION', 'FORM_ACTION', 'GOOGLE_SEARCH_ACTION', 'EMAIL_ACTION', 'JIRA_ACTION', 'ZENDESK_ACTION', 'PIPEDRIVE_LEADS_ACTION', 'HUBSPOT_FORMS_ACTION', 'RAZORPAY_ACTION', 'TWO_STAGE_FALLBACK_ACTION', 'PYSCRIPT_ACTION', 'PROMPT_ACTION', 'DATABASE_ACTION', 'WEB_SEARCH_ACTION', 'LIVE_AGENT_ACTION'", "type": "type_error.enum", } ] @@ -12258,10 +12404,11 @@ def test_update_rule_invalid_event_type(): "PROMPT_ACTION", "DATABASE_ACTION", "WEB_SEARCH_ACTION", + "LIVE_AGENT_ACTION", ] }, "loc": ["body", "steps", 0, "type"], - "msg": "value is not a valid enumeration member; permitted: 'INTENT', 'SLOT', 'FORM_START', 'FORM_END', 'BOT', 'HTTP_ACTION', 'ACTION', 'SLOT_SET_ACTION', 'FORM_ACTION', 'GOOGLE_SEARCH_ACTION', 'EMAIL_ACTION', 'JIRA_ACTION', 'ZENDESK_ACTION', 'PIPEDRIVE_LEADS_ACTION', 'HUBSPOT_FORMS_ACTION', 'RAZORPAY_ACTION', 'TWO_STAGE_FALLBACK_ACTION', 'PYSCRIPT_ACTION', 'PROMPT_ACTION', 'DATABASE_ACTION', 'WEB_SEARCH_ACTION'", + "msg": "value is not a valid enumeration member; permitted: 'INTENT', 'SLOT', 'FORM_START', 'FORM_END', 'BOT', 'HTTP_ACTION', 'ACTION', 'SLOT_SET_ACTION', 'FORM_ACTION', 'GOOGLE_SEARCH_ACTION', 'EMAIL_ACTION', 'JIRA_ACTION', 'ZENDESK_ACTION', 'PIPEDRIVE_LEADS_ACTION', 'HUBSPOT_FORMS_ACTION', 'RAZORPAY_ACTION', 'TWO_STAGE_FALLBACK_ACTION', 'PYSCRIPT_ACTION', 'PROMPT_ACTION', 'DATABASE_ACTION', 'WEB_SEARCH_ACTION', 'LIVE_AGENT_ACTION'", "type": "type_error.enum", } ] @@ -17964,6 +18111,7 @@ def mock_reload_model(*args, **kwargs): "database_action": [], "actions": [], "pyscript_action": [], + "live_agent_action": [], }, ignore_order=True, ) diff --git a/tests/testing_data/system.yaml b/tests/testing_data/system.yaml index d90272d5b..996471398 100644 --- a/tests/testing_data/system.yaml +++ b/tests/testing_data/system.yaml @@ -235,4 +235,7 @@ core: - SpacyNLP - SpacyTokenizer - SpacyEntityExtractor - - SpacyFeaturizer \ No newline at end of file + +live_agent: + url: ${LIVE_AGENT_SERVER_URL:"http://localhost:8000/api/v1"} + auth_token: ${LIVE_AGENT_AUTH_TOKEN:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia2Fpcm9uIn0.EJXuG2n8dy72h9J-8SoIBToVmnB4gpXDGKg_Smcz-C8"} diff --git a/tests/unit_test/chat/chat_test.py b/tests/unit_test/chat/chat_test.py index 7ffce7976..016f660d8 100644 --- a/tests/unit_test/chat/chat_test.py +++ b/tests/unit_test/chat/chat_test.py @@ -598,7 +598,8 @@ def _mock_generate_integration_token(*arge, **kwargs): channel_url = ChatDataProcessor.save_channel_config({ "connector_type": "whatsapp", "config": { "app_secret": "app123", - "access_token": "appsecret123", "verify_token": "integrate_1" + "access_token": "appsecret123", "verify_token": "integrate_1", + "phone_number": "01234567890" }}, bot, "test@chat.com") channel = Channels.objects(bot=bot, connector_type="whatsapp").get() response = DataUtility.get_channel_endpoint(channel) diff --git a/tests/unit_test/data_processor/data_processor_test.py b/tests/unit_test/data_processor/data_processor_test.py index 2590417da..9c8b17839 100644 --- a/tests/unit_test/data_processor/data_processor_test.py +++ b/tests/unit_test/data_processor/data_processor_test.py @@ -982,6 +982,85 @@ def test_delete_prompt_action_not_present(self): user = 'test_user' with pytest.raises(AppException, match=f'Action with name "non_existent_kairon_faq_action" not found'): processor.delete_action('non_existent_kairon_faq_action', bot, user) + + def test_get_live_agent(self): + processor = MongoProcessor() + bot = 'test_bot' + user = 'test_user' + live_agent = processor.get_live_agent(bot=bot) + assert live_agent == [] + + def test_enable_live_agent(self): + processor = MongoProcessor() + bot = 'test_bot' + user = 'test_user' + request_data = { + "bot_response": "connecting to live agent", + "dispatch_bot_response": False, + } + result = processor.enable_live_agent(request_data=request_data, bot=bot, user=user) + assert result is True + + def test_get_live_agent_after_enabled(self): + processor = MongoProcessor() + bot = 'test_bot' + user = 'test_user' + live_agent = processor.get_live_agent(bot=bot) + print(live_agent) + assert live_agent == {'name': 'live_agent_action', + 'bot_response': 'connecting to live agent', + 'dispatch_bot_response': False} + + def test_enable_live_agent_already_exist(self): + processor = MongoProcessor() + bot = 'test_bot' + user = 'test_user' + request_data = { + "bot_response": "connecting to live agent", + "dispatch_bot_response": False, + } + result = processor.enable_live_agent(request_data=request_data, bot=bot, user=user) + assert result is False + + def test_edit_live_agent(self): + processor = MongoProcessor() + bot = 'test_bot' + user = 'test_user' + request_data = { + "bot_response": "connecting to different live agent...", + "dispatch_bot_response": True, + } + result = processor.edit_live_agent(request_data=request_data, bot=bot, user=user) + + live_agent = processor.get_live_agent(bot=bot) + print(live_agent) + assert live_agent == {'name': 'live_agent_action', + 'bot_response': 'connecting to different live agent...', + 'dispatch_bot_response': True} + + def test_disable_live_agent(self): + processor = MongoProcessor() + bot = 'test_bot' + user = 'test_user' + request_data = { + "bot_response": "connecting to different live agent...", + "dispatch_bot_response": True, + } + result = processor.disable_live_agent(bot=bot) + + live_agent = processor.get_live_agent(bot=bot) + assert live_agent == [] + + def test_edit_live_agent_does_not_exist(self): + processor = MongoProcessor() + bot = 'test_bot' + user = 'test_user' + request_data = { + "bot_response": "connecting to different live agent...", + "dispatch_bot_response": True, + } + with pytest.raises(AppException, match=f'Live agent not enabled for the bot'): + result = processor.edit_live_agent(request_data=request_data, bot=bot, user=user) def test_auditlog_event_config_does_not_exist(self): result = MongoProcessor.get_auditlog_event_config("nobot") @@ -10837,7 +10916,7 @@ 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': [] + 'database_action': [], 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [] } def test_add_complex_story_with_action(self): @@ -10860,7 +10939,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': [] + 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [] } def test_add_complex_story(self): @@ -10885,7 +10964,7 @@ 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': [], + 'database_action': [], 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [], 'utterances': ['utter_greet', 'utter_cheer_up', 'utter_did_that_help', @@ -12658,7 +12737,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': [], + 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [], 'utterances': ['utter_offer_help', 'utter_query', 'utter_goodbye', 'utter_feedback', 'utter_default', 'utter_please_rephrase'], 'web_search_action': []}, ignore_order=True) @@ -12767,7 +12846,7 @@ def test_add_rule(self): 'http_action': [], 'google_search_action': [], 'pipedrive_leads_action': [], 'kairon_bot_response': [], 'razorpay_action': [], 'prompt_action': ['gpt_llm_faq'], 'slot_set_action': [], 'email_action': [], 'form_validation_action': [], 'jira_action': [], - 'database_action': [], 'pyscript_action': [], 'web_search_action': [], + 'database_action': [], 'pyscript_action': [], 'web_search_action': [], 'live_agent_action': [], 'utterances': ['utter_greet', 'utter_cheer_up', 'utter_did_that_help',