Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support ActiveLoop event / deprecate FormAction #246

Merged
merged 8 commits into from
Aug 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/246.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Rasa Open Source 2.0 renamed the ``Form`` event to ``ActiveLoop``. The ``ActiveLoop``
event was added to the SDK and support for the ``active_loop`` field in JSON payloads
was added.
6 changes: 6 additions & 0 deletions changelog/246.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Using the ``Form`` event is deprecated. Please use the new ``ActiveLoop`` event instead.
Using the ``active_form`` property of the ``Tracker`` object is now deprecated. Please
use the ``active_loop`` property instead.

The usage of the ``FormAction`` is deprecated. Please see the migration guide
for Rasa Open Source 2.0 for instructions how to migrate your `FormAction`s.
11 changes: 10 additions & 1 deletion rasa_sdk/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,18 @@ def AgentUttered(
return {"event": "agent", "text": text, "data": data, "timestamp": timestamp}


# noinspection PyPep8Naming
def ActiveLoop(name: Optional[Text], timestamp: Optional[float] = None) -> EventType:
return {"event": "active_loop", "name": name, "timestamp": timestamp}


# noinspection PyPep8Naming
def Form(name: Optional[Text], timestamp: Optional[float] = None) -> EventType:
return {"event": "form", "name": name, "timestamp": timestamp}
warnings.warn(
"The `Form` event is deprecated. Please use the `ActiveLoop` event " "instead.",
DeprecationWarning,
)
return ActiveLoop(name, timestamp)


# noinspection PyPep8Naming
Expand Down
38 changes: 23 additions & 15 deletions rasa_sdk/forms.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import logging
import typing
import warnings
from typing import Dict, Text, Any, List, Union, Optional, Tuple

from rasa_sdk import utils
from rasa_sdk.events import SlotSet, Form, EventType
from rasa_sdk.events import SlotSet, EventType, ActiveLoop
from rasa_sdk.interfaces import Action, ActionExecutionRejection

logger = logging.getLogger(__name__)
Expand All @@ -18,6 +19,15 @@


class FormAction(Action):
def __init__(self):
warnings.warn(
"Using the `FormAction` class is deprecated as of Rasa Open "
"Source version 2.0. Please see the migration guide "
"for Rasa Open Source 2.0 for instructions how to migrate.",
FutureWarning,
)
super().__init__()

def name(self) -> Text:
"""Unique identifier of the form"""

Expand Down Expand Up @@ -183,11 +193,9 @@ def intent_is_desired(
mapping_not_intents = requested_slot_mapping.get("not_intent", [])
intent = tracker.latest_message.get("intent", {}).get("name")

intent_not_blacklisted = (
not mapping_intents and intent not in mapping_not_intents
)
intent_not_excluded = not mapping_intents and intent not in mapping_not_intents

return intent_not_blacklisted or intent in mapping_intents
return intent_not_excluded or intent in mapping_intents

def entity_is_desired(
self, other_slot_mapping: Dict[Text, Any], other_slot: Text, tracker: "Tracker"
Expand Down Expand Up @@ -282,7 +290,7 @@ def extract_other_slots(
# check whether the slot should be
# filled from trigger intent mapping
should_fill_trigger_slot = (
tracker.active_form.get("name") != self.name()
tracker.active_loop.get("name") != self.name()
and other_slot_mapping["type"] == "from_trigger_intent"
and self.intent_is_desired(other_slot_mapping, tracker)
)
Expand Down Expand Up @@ -443,7 +451,7 @@ def deactivate(self) -> List[EventType]:
and reset the requested slot"""

logger.debug(f"Deactivating the form '{self.name()}'")
return [Form(None), SlotSet(REQUESTED_SLOT, None)]
return [ActiveLoop(None), SlotSet(REQUESTED_SLOT, None)]

async def submit(
self,
Expand Down Expand Up @@ -507,16 +515,16 @@ async def _activate_if_required(
as any `SlotSet` events from validation of pre-filled slots.
"""

if tracker.active_form.get("name") is not None:
logger.debug(f"The form '{tracker.active_form}' is active")
if tracker.active_loop.get("name") is not None:
logger.debug(f"The form '{tracker.active_loop}' is active")
else:
logger.debug("There is no active form")

if tracker.active_form.get("name") == self.name():
if tracker.active_loop.get("name") == self.name():
return []
else:
logger.debug(f"Activated the form '{self.name()}'")
events = [Form(self.name())]
events = [ActiveLoop(self.name())]

# collect values of required slots filled before activation
prefilled_slots = {}
Expand Down Expand Up @@ -549,10 +557,10 @@ async def _validate_if_required(
- the form is called after `action_listen`
- form validation was not cancelled
"""
# no active_form means that it is called during activation
need_validation = not tracker.active_form or (
# no active_loop means that it is called during activation
need_validation = not tracker.active_loop or (
tracker.latest_action_name == "action_listen"
and tracker.active_form.get("validate", True)
and tracker.active_loop.get("validate", True)
)
if need_validation:
logger.debug(f"Validating user input '{tracker.latest_message}'")
Expand Down Expand Up @@ -592,7 +600,7 @@ async def run(
# validate user input
events.extend(await self._validate_if_required(dispatcher, tracker, domain))
# check that the form wasn't deactivated in validation
if Form(None) not in events:
if ActiveLoop(None) not in events:

# create temp tracker with populated slots from `validate` method
temp_tracker = tracker.copy()
Expand Down
19 changes: 14 additions & 5 deletions rasa_sdk/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import copy
import logging
import warnings
from typing import Any, Dict, Iterator, List, Optional, Text

logger = logging.getLogger(__name__)
Expand All @@ -21,7 +22,7 @@ def from_dict(cls, state: Dict[Text, Any]) -> "Tracker":
state.get("events"),
state.get("paused"),
state.get("followup_action"),
state.get("active_form", {}),
state.get("active_loop", state.get("active_form", {})),
state.get("latest_action_name"),
)

Expand All @@ -33,7 +34,7 @@ def __init__(
events: List[Dict[Text, Any]],
paused: bool,
followup_action: Optional[Text],
active_form: Optional[Dict],
active_loop: Dict[Text, Any],
latest_action_name: Optional[Text],
) -> None:
"""Initialize the tracker."""
Expand All @@ -54,9 +55,17 @@ def __init__(
# "entities": UserUttered.entities,
# "text": text}
self.latest_message = latest_message if latest_message else {}
self.active_form = active_form
self.active_loop = active_loop
self.latest_action_name = latest_action_name

@property
def active_form(self) -> Dict[Text, Any]:
warnings.warn(
"Use of `active_form` is deprecated. Please use `active_loop insteaad.",
DeprecationWarning,
)
return self.active_loop

def current_state(self) -> Dict[Text, Any]:
"""Return the current tracker state as an object."""

Expand All @@ -73,7 +82,7 @@ def current_state(self) -> Dict[Text, Any]:
"paused": self.is_paused(),
"events": self.events,
"latest_input_channel": self.get_latest_input_channel(),
"active_form": self.active_form,
"active_loop": self.active_loop,
"latest_action_name": self.latest_action_name,
}

Expand Down Expand Up @@ -165,7 +174,7 @@ def copy(self) -> "Tracker":
copy.deepcopy(self.events),
self._paused,
self.followup_action,
self.active_form,
self.active_loop,
self.latest_action_name,
)

Expand Down
12 changes: 12 additions & 0 deletions tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,15 @@ def test_reminder_cancelled_correctly(intent):
def test_reminder_cancelled_with_action(intent):
with pytest.warns(FutureWarning):
events.ReminderCancelled(name="utter_something", intent_name=intent)


def test_deprecation_warning_form_event():
form_name = "my form"
timestamp = 123
with pytest.warns(DeprecationWarning):
event = events.Form(form_name, timestamp=timestamp)
assert event == {
"name": form_name,
"event": "active_loop",
"timestamp": timestamp,
}
19 changes: 12 additions & 7 deletions tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Type, Text, Dict, Any, List, Optional

from rasa_sdk import Tracker, ActionExecutionRejection
from rasa_sdk.events import SlotSet, Form
from rasa_sdk.events import SlotSet, ActiveLoop
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk.forms import FormAction, REQUESTED_SLOT

Expand Down Expand Up @@ -1002,7 +1002,7 @@ def validate_some_slot(self, value, dispatcher, tracker, domain):
)
# check that the form was activated and prefilled slots were validated
assert events == [
Form("some_form"),
ActiveLoop("some_form"),
SlotSet("some_slot", "validated_value"),
SlotSet("some_other_slot", "some_other_value"),
]
Expand All @@ -1013,7 +1013,7 @@ def validate_some_slot(self, value, dispatcher, tracker, domain):

# check that entities picked up in input overwrite prefilled slots
assert events == [
Form("some_form"),
ActiveLoop("some_form"),
SlotSet("some_slot", "validated_value"),
SlotSet("some_other_slot", "some_other_value"),
SlotSet("some_slot", None),
Expand Down Expand Up @@ -1143,7 +1143,7 @@ def required_slots(_tracker):
dispatcher=None, tracker=tracker, domain=None
)
# check that the form was activated
assert events == [Form("some_form")]
assert events == [ActiveLoop("some_form")]

tracker = Tracker(
"default",
Expand Down Expand Up @@ -1273,7 +1273,7 @@ async def submit(self, _dispatcher, _tracker, _domain):
events = await form.run(dispatcher=dispatcher, tracker=tracker, domain=None)
# check that the form was activated and validation was performed
assert events == [
Form("some_form"),
ActiveLoop("some_form"),
SlotSet("some_other_slot", "some_other_value"),
SlotSet(REQUESTED_SLOT, "some_slot"),
]
Expand Down Expand Up @@ -1308,7 +1308,7 @@ async def submit(self, _dispatcher, _tracker, _domain):
events = await form.run(dispatcher=dispatcher, tracker=tracker, domain=None)
# check that the form was activated and validation was performed
assert events == [
Form("some_form"),
ActiveLoop("some_form"),
SlotSet("some_other_slot", "some_other_value"),
SlotSet(REQUESTED_SLOT, "some_slot"),
]
Expand Down Expand Up @@ -1389,7 +1389,7 @@ async def test_early_deactivation(form_class: Type[FormAction]):
events = await form.run(dispatcher=None, tracker=tracker, domain=None)

# check that form was deactivated before requesting next slot
assert events == [Form(None), SlotSet(REQUESTED_SLOT, None)]
assert events == [ActiveLoop(None), SlotSet(REQUESTED_SLOT, None)]
assert SlotSet(REQUESTED_SLOT, "some_other_slot") not in events


Expand Down Expand Up @@ -1430,3 +1430,8 @@ async def test_submit(form_class: Type[FormAction]):
events = await form.run(dispatcher=None, tracker=tracker, domain=None)

assert events[0]["value"] == 42


def test_form_deprecation():
with pytest.warns(FutureWarning):
FormAction()
51 changes: 51 additions & 0 deletions tests/test_interfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from typing import Dict

import pytest

from rasa_sdk.interfaces import Tracker


@pytest.mark.parametrize(
"active_loop",
[
{},
{"name": "some loop"},
{"name": "some loop", "validate": True},
{"name": "✏️", "rejected": False},
{
"name": "✏️",
"validate": True,
"trigger_message": {"intent": {}, "intent_ranking": []},
},
],
)
def test_tracker_active_loop_parsing(active_loop: Dict):
state = {"events": [], "sender_id": "old", "active_loop": active_loop}
tracker = Tracker.from_dict(state)

assert tracker.active_loop == active_loop


def test_deprecation_warning_active_form():
form = {"name": "my form"}
state = {"events": [], "sender_id": "old", "active_loop": form}
tracker = Tracker.from_dict(state)

with pytest.warns(DeprecationWarning):
assert tracker.active_form == form


def test_parsing_of_trackers_with_old_active_form_field():
form = {"name": "my form"}
state = {"events": [], "sender_id": "old", "active_form": form}
tracker = Tracker.from_dict(state)

assert tracker.active_loop == form


def test_active_loop_in_tracker_state():
form = {"name": "my form"}
state = {"events": [], "sender_id": "old", "active_loop": form}
tracker = Tracker.from_dict(state)

assert tracker.current_state()["active_loop"] == form