import logging
import typing
from typing import List, Text, Optional, Dict, Any
import requests
import copy
import rasa_core
from rasa_core import events
from rasa_core.constants import (
    DOCS_BASE_URL,
    DEFAULT_REQUEST_TIMEOUT,
    REQUESTED_SLOT, USER_INTENT_OUT_OF_SCOPE)
from rasa_core.events import (UserUtteranceReverted, UserUttered,
                              ActionExecuted, Event)
from rasa_core.utils import EndpointConfig
if typing.TYPE_CHECKING:
    from rasa_core.trackers import DialogueStateTracker
    from rasa_core.dispatcher import Dispatcher
    from rasa_core.domain import Domain
logger = logging.getLogger(__name__)
ACTION_LISTEN_NAME = "action_listen"
ACTION_RESTART_NAME = "action_restart"
ACTION_DEFAULT_FALLBACK_NAME = "action_default_fallback"
ACTION_DEACTIVATE_FORM_NAME = "action_deactivate_form"
ACTION_REVERT_FALLBACK_EVENTS_NAME = 'action_revert_fallback_events'
ACTION_DEFAULT_ASK_AFFIRMATION_NAME = 'action_default_ask_affirmation'
ACTION_DEFAULT_ASK_REPHRASE_NAME = 'action_default_ask_rephrase'
ACTION_BACK_NAME = 'action_back'
def default_actions() -> List['Action']:
    """List default actions."""
    return [ActionListen(), ActionRestart(),
            ActionDefaultFallback(), ActionDeactivateForm(),
            ActionRevertFallbackEvents(), ActionDefaultAskAffirmation(),
            ActionDefaultAskRephrase(), ActionBack()]
def default_action_names() -> List[Text]:
    """List default action names."""
    return [a.name() for a in default_actions()]
def combine_user_with_default_actions(user_actions):
    # remove all user actions that overwrite default actions
    # this logic is a bit reversed, you'd think that we should remove
    # the action name from the default action names if the user overwrites
    # the action, but there are some locations in the code where we
    # implicitly assume that e.g. "action_listen" is always at location
    # 0 in this array. to keep it that way, we remove the duplicate
    # action names from the users list instead of the defaults
    unique_user_actions = [a
                           for a in user_actions
                           if a not in default_action_names()]
    return default_action_names() + unique_user_actions
def ensure_action_name_uniqueness(action_names: List[Text]) -> None:
    """Check and raise an exception if there are two actions with same name."""
    unique_action_names = set()  # used to collect unique action names
    for a in action_names:
        if a in unique_action_names:
            raise ValueError(
                "Action names are not unique! Found two actions with name"
                " '{}'. Either rename or remove one of them.".format(a))
        else:
            unique_action_names.add(a)
def action_from_name(name: Text, action_endpoint: Optional[EndpointConfig],
                     user_actions: List[Text]) -> 'Action':
    """Return an action instance for the name."""
    defaults = {a.name(): a for a in default_actions()}
    if name in defaults and name not in user_actions:
        return defaults.get(name)
    elif name.startswith("utter_"):
        return UtterAction(name)
    else:
        return RemoteAction(name, action_endpoint)
def actions_from_names(action_names: List[Text],
                       action_endpoint: Optional[EndpointConfig],
                       user_actions: List[Text]) -> List['Action']:
    """Converts the names of actions into class instances."""
    return [action_from_name(name, action_endpoint, user_actions)
            for name in action_names]
class Action(object):
    """Next action to be taken in response to a dialogue state."""
    def name(self) -> Text:
        """Unique identifier of this simple action."""
        raise NotImplementedError
    def run(self, dispatcher: 'Dispatcher', tracker: 'DialogueStateTracker',
            domain: 'Domain') -> List['Event']:
        """
        Execute the side effects of this action.
        Args:
            dispatcher (Dispatcher): the dispatcher which is used to send
                messages back to the user. Use ``dispatcher.utter_message()``
                or any other :class:`rasa_core.dispatcher.Dispatcher` method.
            tracker (DialogueStateTracker): the state tracker for the current
                user. You can access slot values using
                ``tracker.get_slot(slot_name)`` and the most recent user
                message is ``tracker.latest_message.text``.
            domain (Domain): the bot's domain
        Returns:
            List[Event]: A list of :class:`rasa_core.events.Event` instances
        """
        raise NotImplementedError
    def __str__(self) -> Text:
        return "Action('{}')".format(self.name())
class UtterAction(Action):
    """An action which only effect is to utter a template when it is run.
    Both, name and utter template, need to be specified using
    the `name` method."""
    def __init__(self, name):
        self._name = name
    def run(self, dispatcher, tracker, domain):
        """Simple run implementation uttering a (hopefully defined)
           template."""
        dispatcher.utter_template(self.name(),
                                  tracker)
        return []
    def name(self) -> Text:
        return self._name
    def __str__(self) -> Text:
        return "UtterAction('{}')".format(self.name())
class ActionBack(Action):
    """Revert the tracker state by two user utterances."""
    def name(self) -> Text:
        return ACTION_BACK_NAME
    def run(self, dispatcher, tracker, domain):
        # only utter the template if it is available
        dispatcher.utter_template("utter_back", tracker,
                                  silent_fail=True)
        return [UserUtteranceReverted(), UserUtteranceReverted()]
class ActionListen(Action):
    """The first action in any turn - bot waits for a user message.
    The bot should stop taking further actions and wait for the user to say
    something."""
    def name(self) -> Text:
        return ACTION_LISTEN_NAME
    def run(self, dispatcher, tracker, domain):
        return []
class ActionRestart(Action):
    """Resets the tracker to its initial state.
    Utters the restart template if available."""
    def name(self) -> Text:
        return ACTION_RESTART_NAME
    def run(self, dispatcher, tracker, domain):
        from rasa_core.events import Restarted
        # only utter the template if it is available
        dispatcher.utter_template("utter_restart", tracker,
                                  silent_fail=True)
        return [Restarted()]
[docs]class ActionDefaultFallback(Action):
    """Executes the fallback action and goes back to the previous state
    of the dialogue"""
    def name(self) -> Text:
        return ACTION_DEFAULT_FALLBACK_NAME
    def run(self, dispatcher, tracker, domain):
        from rasa_core.events import UserUtteranceReverted
        dispatcher.utter_template("utter_default", tracker,
                                  silent_fail=True)
        return [UserUtteranceReverted()] 
class ActionDeactivateForm(Action):
    """Deactivates a form"""
    def name(self) -> Text:
        return ACTION_DEACTIVATE_FORM_NAME
    def run(self, dispatcher, tracker, domain):
        from rasa_core.events import Form, SlotSet
        return [Form(None), SlotSet(REQUESTED_SLOT, None)]
class RemoteAction(Action):
    def __init__(self, name: Text,
                 action_endpoint: Optional[EndpointConfig]) -> None:
        self._name = name
        self.action_endpoint = action_endpoint
    def _action_call_format(self, tracker: 'DialogueStateTracker',
                            domain: 'Domain') -> Dict[Text, Any]:
        """Create the request json send to the action server."""
        from rasa_core.trackers import EventVerbosity
        tracker_state = tracker.current_state(EventVerbosity.ALL)
        return {
            "next_action": self._name,
            "sender_id": tracker.sender_id,
            "tracker": tracker_state,
            "domain": domain.as_dict(),
            "version": rasa_core.__version__
        }
    @staticmethod
    def action_response_format_spec():
        """Expected response schema for an Action endpoint.
        Used for validation of the response returned from the
        Action endpoint."""
        return {
            "type": "object",
            "properties": {
                "events": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "event": {"type": "string"}
                        }
                    }
                },
                "responses": {
                    "type": "array",
                    "items": {
                        "type": "object",
                    }
                }
            },
        }
    def _validate_action_result(self, result):
        from jsonschema import validate
        from jsonschema import ValidationError
        try:
            validate(result, self.action_response_format_spec())
            return True
        except ValidationError as e:
            e.message += (
                ". Failed to validate Action server response from API, "
                "make sure your response from the Action endpoint is valid. "
                "For more information about the format visit "
                "{}/customactions/".format(DOCS_BASE_URL))
            raise e
    @staticmethod
    def _utter_responses(responses: List[Dict[Text, Any]],
                         dispatcher: 'Dispatcher',
                         tracker: 'DialogueStateTracker'
                         ) -> None:
        """Use the responses generated by the action endpoint and utter them.
        Uses the normal dispatcher to utter the responses from the action
        endpoint."""
        for response in responses:
            if "template" in response:
                kwargs = response.copy()
                del kwargs["template"]
                draft = dispatcher.nlg.generate(
                    response["template"],
                    tracker,
                    dispatcher.output_channel.name(),
                    **kwargs)
                if not draft:
                    continue
                del response["template"]
            else:
                draft = {}
            if "buttons" in response:
                if "buttons" not in draft:
                    draft["buttons"] = []
                draft["buttons"].extend(response["buttons"])
                del response["buttons"]
            draft.update(response)
            dispatcher.utter_response(draft)
    def run(self, dispatcher, tracker, domain):
        json = self._action_call_format(tracker, domain)
        if not self.action_endpoint:
            raise Exception("The model predicted the custom action '{}' "
                            "but you didn't configure an endpoint to "
                            "run this custom action. Please take a look at "
                            "the docs and set an endpoint configuration. "
                            "{}/customactions/"
                            "".format(self.name(), DOCS_BASE_URL))
        try:
            logger.debug("Calling action endpoint to run action '{}'."
                         "".format(self.name()))
            response = self.action_endpoint.request(
                json=json, method="post", timeout=DEFAULT_REQUEST_TIMEOUT)
            if response.status_code == 400:
                response_data = response.json()
                exception = ActionExecutionRejection(
                    response_data["action_name"],
                    response_data.get("error")
                )
                logger.debug(exception.message)
                raise exception
            response.raise_for_status()
            response_data = response.json()
            self._validate_action_result(response_data)
        except requests.exceptions.ConnectionError as e:
            logger.error("Failed to run custom action '{}'. Couldn't connect "
                         "to the server at '{}'. Is the server running? "
                         "Error: {}".format(self.name(),
                                            self.action_endpoint.url,
                                            e))
            raise Exception("Failed to execute custom action.")
        except requests.exceptions.HTTPError as e:
            logger.error("Failed to run custom action '{}'. Action server "
                         "responded with a non 200 status code of {}. "
                         "Make sure your action server properly runs actions "
                         "and returns a 200 once the action is executed. "
                         "Error: {}".format(self.name(),
                                            e.response.status_code,
                                            e))
            raise Exception("Failed to execute custom action.")
        events_json = response_data.get("events", [])
        responses = response_data.get("responses", [])
        self._utter_responses(responses, dispatcher, tracker)
        evts = events.deserialise_events(events_json)
        return evts
    def name(self) -> Text:
        return self._name
class ActionExecutionRejection(Exception):
    """Raising this exception will allow other policies
        to predict a different action"""
    def __init__(self, action_name, message=None):
        self.action_name = action_name
        self.message = (message or
                        "Custom action '{}' rejected to run"
                        "".format(action_name))
    def __str__(self):
        return self.message
class ActionRevertFallbackEvents(Action):
    """Reverts events which were done during the `TwoStageFallbackPolicy`.
       This reverts user messages and bot utterances done during a fallback
       of the `TwoStageFallbackPolicy`. By doing so it is not necessary to
       write custom stories for the different paths, but only of the happy
       path.
    """
    def name(self) -> Text:
        return ACTION_REVERT_FALLBACK_EVENTS_NAME
    def run(self, dispatcher: 'Dispatcher', tracker: 'DialogueStateTracker',
            domain: 'Domain') -> List[Event]:
        from rasa_core.policies.two_stage_fallback import has_user_rephrased
        revert_events = []
        # User rephrased
        if has_user_rephrased(tracker):
            revert_events = _revert_successful_rephrasing(tracker)
        # User affirmed
        elif has_user_affirmed(tracker):
            revert_events = _revert_affirmation_events(tracker)
        return revert_events
def has_user_affirmed(tracker: 'DialogueStateTracker') -> bool:
    return tracker.last_executed_action_has(
        ACTION_DEFAULT_ASK_AFFIRMATION_NAME)
def _revert_affirmation_events(tracker: 'DialogueStateTracker') -> List[Event]:
    revert_events = _revert_single_affirmation_events()
    last_user_event = tracker.get_last_event_for(UserUttered)
    last_user_event = copy.deepcopy(last_user_event)
    last_user_event.parse_data['intent']['confidence'] = 1.0
    # User affirms the rephrased intent
    rephrased_intent = tracker.last_executed_action_has(
        name=ACTION_DEFAULT_ASK_REPHRASE_NAME,
        skip=1)
    if rephrased_intent:
        revert_events += _revert_rephrasing_events()
    return revert_events + [last_user_event]
def _revert_single_affirmation_events() -> List[Event]:
    return [UserUtteranceReverted(),  # revert affirmation and request
            # revert original intent (has to be re-added later)
            UserUtteranceReverted(),
            # add action listen intent
            ActionExecuted(action_name=ACTION_LISTEN_NAME)]
def _revert_successful_rephrasing(tracker) -> List[Event]:
    last_user_event = tracker.get_last_event_for(UserUttered)
    last_user_event = copy.deepcopy(last_user_event)
    return _revert_rephrasing_events() + [last_user_event]
def _revert_rephrasing_events() -> List[Event]:
    return [UserUtteranceReverted(),  # remove rephrasing
            # remove feedback and rephrase request
            UserUtteranceReverted(),
            # remove affirmation request and false intent
            UserUtteranceReverted(),
            # replace action with action listen
            ActionExecuted(action_name=ACTION_LISTEN_NAME)]
class ActionDefaultAskAffirmation(Action):
    """Default implementation which asks the user to affirm his intent.
       It is suggested to overwrite this default action with a custom action
       to have more meaningful prompts for the affirmations. E.g. have a
       description of the intent instead of its identifier name.
    """
    def name(self) -> Text:
        return ACTION_DEFAULT_ASK_AFFIRMATION_NAME
    def run(self, dispatcher: 'Dispatcher', tracker: 'DialogueStateTracker',
            domain: 'Domain') -> List[Event]:
        intent_to_affirm = tracker.latest_message.intent.get('name')
        affirmation_message = "Did you mean '{}'?".format(intent_to_affirm)
        dispatcher.utter_button_message(text=affirmation_message,
                                        buttons=[{'title': 'Yes',
                                                  'payload': '/{}'.format(
                                                      intent_to_affirm)},
                                                 {'title': 'No',
                                                  'payload': '/{}'.format(
                                                      USER_INTENT_OUT_OF_SCOPE)}
                                                 ])
        return []
class ActionDefaultAskRephrase(Action):
    """Default implementation which asks the user to rephrase his intent."""
    def name(self) -> Text:
        return ACTION_DEFAULT_ASK_REPHRASE_NAME
    def run(self, dispatcher: 'Dispatcher', tracker: 'DialogueStateTracker',
            domain: 'Domain') -> List[Event]:
        dispatcher.utter_template("utter_ask_rephrase", tracker,
                                  silent_fail=True)
        return []