Skip to content

Commit

Permalink
Merge pull request #246 from RasaHQ/form-to-loop-event-rename
Browse files Browse the repository at this point in the history
support `ActiveLoop` event / deprecate `FormAction`
  • Loading branch information
wochinge authored Aug 21, 2020
2 parents e898579 + 9fdc456 commit eaba4f1
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 28 deletions.
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

0 comments on commit eaba4f1

Please sign in to comment.