Skip to content

Commit

Permalink
Merge a1d7243 into 322da75
Browse files Browse the repository at this point in the history
  • Loading branch information
ricwo committed Dec 11, 2019
2 parents 322da75 + a1d7243 commit ff92b60
Show file tree
Hide file tree
Showing 39 changed files with 1,323 additions and 225 deletions.
25 changes: 25 additions & 0 deletions changelog/4830.feature.rst
@@ -0,0 +1,25 @@
Added conversation sessions to trackers.

A conversation session represents the dialog between the assistant and a user.
Conversation sessions can begin in three ways: 1. the user begins the conversation
with the assistant, 2. the user sends their first message after a configurable period
of inactivity, or 3. a manual session start is triggered with the ``/session_start``
intent message. The period of inactivity after which a new conversation session is
triggered is defined in the domain using the ``session_expiration_time`` key in the
``session_config`` section. The introduction of conversation sessions comprises the
following changes:

- Added a new event ``SessionStarted`` that marks the beginning of a new conversation
session.
- Added a new default action ``ActionSessionStart``. This action takes all
``SlotSet`` events from the previous session and applies it to the next session.
- Added a new default intent ``session_start`` which triggers the start of a new
conversation session.
- ``SQLTrackerStore`` and ``MongoTrackerStore`` only retrieve
events from the last session from the database.


.. note::

The session behaviour is disabled for existing projects, i.e. existing domains
without session config section.
4 changes: 4 additions & 0 deletions data/test_domains/duplicate_intents.yml
Expand Up @@ -26,3 +26,7 @@ actions:
- utter_default
- utter_greet
- utter_goodbye

session_config:
session_expiration_time: 60
carry_over_slots_to_new_session: true
22 changes: 22 additions & 0 deletions docs/api/events.rst
Expand Up @@ -271,3 +271,25 @@ Log an executed action
.. literalinclude:: ../../rasa/core/events/__init__.py
:dedent: 4
:pyobject: ActionExecuted.apply_to

Start a new conversation session
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Short: Marks the beginning of a new conversation session. Resets the tracker and
triggers an ``ActionSessionStart`` which by default applies the existing
``SlotSet`` events to the new session.

:JSON:
.. literalinclude:: ../../tests/core/test_events.py
:start-after: # DOCS MARKER ActionExecuted
:dedent: 4
:end-before: # DOCS END
:Class:
.. autoclass:: rasa.core.events.SessionStarted

:Effect:
When added to a tracker, this is the code used to update the tracker:

.. literalinclude:: ../../rasa/core/events/__init__.py
:dedent: 4
:pyobject: SessionStarted.apply_to
10 changes: 10 additions & 0 deletions docs/core/actions.rst
Expand Up @@ -222,6 +222,16 @@ There are eight default actions:
| | if the :ref:`mapping-policy` is included in |
| | the policy configuration. |
+-----------------------------------+------------------------------------------------+
| ``action_session_start`` | Start a new conversation session. Take all set |
| | slots, mark the beginning of a new conversation|
| | session and re-apply the existing ``SlotSet`` |
| | events. This action is triggered automatically |
| | after an inactivity period defined by the |
| | ``session_expiration_time`` parameter in the |
| | domain's session config. Can be triggered |
| | manually during a conversation by entering |
| | ``/session_start``. |
+-----------------------------------+------------------------------------------------+
| ``action_default_fallback`` | Undo the last user message (as if the user did |
| | not send it and the bot did not react) and |
| | utter a message that the bot did not |
Expand Down
4 changes: 4 additions & 0 deletions rasa/cli/initial_project/domain.yml
Expand Up @@ -34,3 +34,7 @@ templates:

utter_iamabot:
- text: "I am a bot, powered by Rasa."

session_config:
session_expiration_time: 60
carry_over_slots_to_new_session: true
5 changes: 2 additions & 3 deletions rasa/cli/x.py
Expand Up @@ -2,12 +2,11 @@
import asyncio
import importlib.util
import logging
import warnings
import os
import signal
import traceback
from multiprocessing import get_context
from typing import List, Text, Optional, Tuple, Union, Iterable
from typing import List, Text, Optional, Tuple, Iterable

import aiohttp
import ruamel.yaml as yaml
Expand Down Expand Up @@ -328,7 +327,7 @@ def rasa_x(args: argparse.Namespace):
async def _pull_runtime_config_from_server(
config_endpoint: Optional[Text],
attempts: int = 60,
wait_time_between_pulls: Union[int, float] = 5,
wait_time_between_pulls: float = 5,
keys: Iterable[Text] = ("endpoints", "credentials"),
) -> Optional[List[Text]]:
"""Pull runtime config from `config_endpoint`.
Expand Down
3 changes: 3 additions & 0 deletions rasa/constants.py
Expand Up @@ -46,3 +46,6 @@
DEFAULT_SANIC_WORKERS = 1
ENV_SANIC_WORKERS = "SANIC_WORKERS"
ENV_SANIC_BACKLOG = "SANIC_BACKLOG"

DEFAULT_SESSION_EXPIRATION_TIME_IN_MINUTES = 60
DEFAULT_CARRY_OVER_SLOTS_TO_NEW_SESSION = True
49 changes: 48 additions & 1 deletion rasa/core/actions/action.py
Expand Up @@ -2,7 +2,7 @@
import json
import logging
import typing
from typing import List, Text, Optional, Dict, Any
from typing import List, Text, Optional, Dict, Any, Generator

import aiohttp

Expand Down Expand Up @@ -37,13 +37,16 @@
from rasa.core.domain import Domain
from rasa.core.nlg import NaturalLanguageGenerator
from rasa.core.channels.channel import OutputChannel
from rasa.core.events import SlotSet

logger = logging.getLogger(__name__)

ACTION_LISTEN_NAME = "action_listen"

ACTION_RESTART_NAME = "action_restart"

ACTION_SESSION_START_NAME = "action_session_start"

ACTION_DEFAULT_FALLBACK_NAME = "action_default_fallback"

ACTION_DEACTIVATE_FORM_NAME = "action_deactivate_form"
Expand All @@ -62,6 +65,7 @@ def default_actions() -> List["Action"]:
return [
ActionListen(),
ActionRestart(),
ActionSessionStart(),
ActionDefaultFallback(),
ActionDeactivateForm(),
ActionRevertFallbackEvents(),
Expand Down Expand Up @@ -331,6 +335,49 @@ async def run(
return evts + [Restarted()]


class ActionSessionStart(Action):
"""Applies a conversation session start.
Takes all `SlotSet` events from the previous session and applies them to the new
session.
"""

def name(self) -> Text:
return ACTION_SESSION_START_NAME

@staticmethod
def _slot_set_events_from_tracker(
tracker: "DialogueStateTracker",
) -> List["SlotSet"]:
"""Fetch SlotSet events from tracker and carry over key, value and metadata."""

from rasa.core.events import SlotSet

return [
SlotSet(key=event.key, value=event.value, metadata=event.metadata)
for event in tracker.applied_events()
if isinstance(event, SlotSet)
]

async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
from rasa.core.events import SessionStarted

_events = [SessionStarted()]

if domain.session_config.carry_over_slots:
_events.extend(self._slot_set_events_from_tracker(tracker))

_events.append(ActionExecuted(ACTION_LISTEN_NAME))

return _events


class ActionDefaultFallback(ActionUtterTemplate):
"""Executes the fallback action and goes back to the previous state
of the dialogue"""
Expand Down
6 changes: 4 additions & 2 deletions rasa/core/agent.py
Expand Up @@ -486,11 +486,13 @@ def noop(_):
return await processor.handle_message(message)

# noinspection PyUnusedLocal
def predict_next(self, sender_id: Text, **kwargs: Any) -> Optional[Dict[Text, Any]]:
async def predict_next(
self, sender_id: Text, **kwargs: Any
) -> Optional[Dict[Text, Any]]:
"""Handle a single message."""

processor = self.create_processor()
return processor.predict_next(sender_id)
return await processor.predict_next(sender_id)

# noinspection PyUnusedLocal
async def log_message(
Expand Down
6 changes: 3 additions & 3 deletions rasa/core/brokers/pika.py
Expand Up @@ -29,7 +29,7 @@ def initialise_pika_connection(
password: Text,
port: Union[Text, int] = 5672,
connection_attempts: int = 20,
retry_delay_in_seconds: Union[int, float] = 5,
retry_delay_in_seconds: float = 5,
) -> "BlockingConnection":
"""Create a Pika `BlockingConnection`.
Expand Down Expand Up @@ -60,7 +60,7 @@ def _get_pika_parameters(
password: Text,
port: Union[Text, int] = 5672,
connection_attempts: int = 20,
retry_delay_in_seconds: Union[int, float] = 5,
retry_delay_in_seconds: float = 5,
) -> "Parameters":
"""Create Pika `Parameters`.
Expand Down Expand Up @@ -135,7 +135,7 @@ def initialise_pika_channel(
password: Text,
port: Union[Text, int] = 5672,
connection_attempts: int = 20,
retry_delay_in_seconds: Union[int, float] = 5,
retry_delay_in_seconds: float = 5,
) -> "BlockingChannel":
"""Initialise a Pika channel with a durable queue.
Expand Down
2 changes: 2 additions & 0 deletions rasa/core/constants.py
Expand Up @@ -27,6 +27,8 @@

USER_INTENT_RESTART = "restart"

USER_INTENT_SESSION_START = "session_start"

USER_INTENT_BACK = "back"

USER_INTENT_OUT_OF_SCOPE = "out_of_scope"
Expand Down
57 changes: 53 additions & 4 deletions rasa/core/domain.py
Expand Up @@ -5,13 +5,17 @@
import os
import typing
from pathlib import Path
from typing import Any, Dict, List, Optional, Text, Tuple, Union, Set
from typing import Any, Dict, List, Optional, Text, Tuple, Union, Set, NamedTuple

import rasa.core.constants
import rasa.utils.common as common_utils
import rasa.utils.io
from rasa.cli.utils import bcolors
from rasa.constants import DOMAIN_SCHEMA_FILE
from rasa.constants import (
DOMAIN_SCHEMA_FILE,
DEFAULT_SESSION_EXPIRATION_TIME_IN_MINUTES,
DEFAULT_CARRY_OVER_SLOTS_TO_NEW_SESSION,
)
from rasa.core import utils
from rasa.core.actions import action # pytype: disable=pyi-error
from rasa.core.actions.action import Action # pytype: disable=pyi-error
Expand All @@ -32,6 +36,10 @@
PREV_PREFIX = "prev_"
ACTIVE_FORM_PREFIX = "active_form_"

CARRY_OVER_SLOTS_KEY = "carry_over_slots_to_new_session"
SESSION_EXPIRATION_TIME_KEY = "session_expiration_time"
SESSION_CONFIG_KEY = "session_config"

if typing.TYPE_CHECKING:
from rasa.core.trackers import DialogueStateTracker

Expand All @@ -47,6 +55,19 @@ def __str__(self):
return bcolors.FAIL + self.message + bcolors.ENDC


class SessionConfig(NamedTuple):
session_expiration_time: float # in minutes
carry_over_slots: bool

@staticmethod
def default() -> "SessionConfig":
# TODO: 2.0, reconsider how to apply sessions to old projects
return SessionConfig(0, DEFAULT_CARRY_OVER_SLOTS_TO_NEW_SESSION)

def are_sessions_enabled(self) -> bool:
return self.session_expiration_time > 0


class Domain:
"""The domain specifies the universe in which the bot's policy acts.
Expand Down Expand Up @@ -109,6 +130,7 @@ def from_dict(cls, data: Dict) -> "Domain":
utter_templates = cls.collect_templates(data.get("templates", {}))
slots = cls.collect_slots(data.get("slots", {}))
additional_arguments = data.get("config", {})
session_config = cls._get_session_config(data.get(SESSION_CONFIG_KEY, {}))
intents = data.get("intents", {})

return cls(
Expand All @@ -118,9 +140,31 @@ def from_dict(cls, data: Dict) -> "Domain":
utter_templates,
data.get("actions", []),
data.get("forms", []),
session_config=session_config,
**additional_arguments,
)

@staticmethod
def _get_session_config(session_config: Dict) -> SessionConfig:
session_expiration_time = session_config.get(SESSION_EXPIRATION_TIME_KEY)

# TODO: 2.0 reconsider how to apply sessions to old projects and legacy trackers
if session_expiration_time is None:
warnings.warn(
"No tracker session configuration was found in the loaded domain. "
"Domains without a session config will automatically receive a "
"session expiration time of 60 minutes in Rasa version 2.0 if not "
"configured otherwise.",
FutureWarning,
)
session_expiration_time = 0

carry_over_slots = session_config.get(
CARRY_OVER_SLOTS_KEY, DEFAULT_CARRY_OVER_SLOTS_TO_NEW_SESSION
)

return SessionConfig(session_expiration_time, carry_over_slots)

@classmethod
def from_directory(cls, path: Text) -> "Domain":
"""Loads and merges multiple domain files recursively from a directory tree."""
Expand Down Expand Up @@ -278,13 +322,15 @@ def __init__(
action_names: List[Text],
form_names: List[Text],
store_entities_as_slots: bool = True,
session_config: SessionConfig = SessionConfig.default(),
) -> None:

self.intent_properties = self.collect_intent_properties(intents)
self.entities = entities
self.form_names = form_names
self.slots = slots
self.templates = templates
self.session_config = session_config

# only includes custom actions and utterance actions
self.user_actions = action_names
Expand Down Expand Up @@ -651,10 +697,13 @@ def _slot_definitions(self) -> Dict[Any, Dict[str, Any]]:
return {slot.name: slot.persistence_info() for slot in self.slots}

def as_dict(self) -> Dict[Text, Any]:
additional_config = {"store_entities_as_slots": self.store_entities_as_slots}

return {
"config": additional_config,
"config": {"store_entities_as_slots": self.store_entities_as_slots},
SESSION_CONFIG_KEY: {
SESSION_EXPIRATION_TIME_KEY: self.session_config.session_expiration_time,
CARRY_OVER_SLOTS_KEY: self.session_config.carry_over_slots,
},
"intents": [{k: v} for k, v in self.intent_properties.items()],
"entities": self.entities,
"slots": self._slot_definitions(),
Expand Down

0 comments on commit ff92b60

Please sign in to comment.