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

Extract slots with mapping conditions during form activation #11149

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
1 change: 1 addition & 0 deletions changelog/11149.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix the extraction of values for slots with mapping conditions from trigger intents that activate a form, which was possible in `2.x`.
57 changes: 53 additions & 4 deletions rasa/core/actions/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from rasa.core.actions.action import ActionExecutionRejection, RemoteAction
from rasa.shared.core.constants import (
ACTION_EXTRACT_SLOTS,
ACTION_LISTEN_NAME,
REQUESTED_SLOT,
LOOP_INTERRUPTED,
Expand Down Expand Up @@ -566,9 +567,10 @@ async def activate(
) -> List[Event]:
"""Activate form if the form is called for the first time.

If activating, validate any required slots that were filled before
form activation and return `Form` event with the name of the form, as well
as any `SlotSet` events from validation of pre-filled slots.
If activating, run action_extract_slots to fill slots with
mapping conditions from trigger intents.
Validate any required slots that can be filled, and return any `SlotSet`
events from the extraction and validation of these pre-filled slots.

Args:
output_channel: The output channel which can be used to send messages
Expand All @@ -584,6 +586,26 @@ async def activate(
# collect values of required slots filled before activation
prefilled_slots = {}

action_extract_slots = action.action_for_name_or_text(
ACTION_EXTRACT_SLOTS, domain, self.action_endpoint
)

logger.debug(
f"Executing default action '{ACTION_EXTRACT_SLOTS}' at form activation."
)

extraction_events = await action_extract_slots.run(
output_channel, nlg, tracker, domain
)

events_as_str = "\n".join(str(e) for e in extraction_events)
logger.debug(
f"The execution of '{ACTION_EXTRACT_SLOTS}' resulted in "
f"these events: {events_as_str}."
)
ancalita marked this conversation as resolved.
Show resolved Hide resolved

tracker.update_with_events(extraction_events, domain)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any reason why this is called here and at the same time the function returns the events? As far as I understand, the processor will update the tracker with the events returned by this function

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to update the tracker immediately in order for the tracker to update its tracker.slots attribute, which gets used in the for loop below (L610-612) to extract the prefilled slots. Because the tracker is temporary, it won't duplicate the events in the actual tracker that the processor updates.


for slot_name in self.required_slots(domain):
if not self._should_request_slot(tracker, slot_name):
prefilled_slots[slot_name] = tracker.get_slot(slot_name)
Expand All @@ -593,10 +615,21 @@ async def activate(
return []

logger.debug(f"Validating pre-filled required slots: {prefilled_slots}")
return await self.validate_slots(

validated_events = await self.validate_slots(
prefilled_slots, tracker, domain, output_channel, nlg
)

validated_slot_names = [
event.key for event in validated_events if isinstance(event, SlotSet)
]

return validated_events + [
event
for event in extraction_events
if isinstance(event, SlotSet) and event.key not in validated_slot_names
]
ancalita marked this conversation as resolved.
Show resolved Hide resolved

async def do(
self,
output_channel: "OutputChannel",
Expand All @@ -605,6 +638,7 @@ async def do(
domain: "Domain",
events_so_far: List[Event],
) -> List[Event]:
"""Executes form loop after activation."""
events = await self._validate_if_required(tracker, domain, output_channel, nlg)

if not self._user_rejected_manually(events):
Expand Down Expand Up @@ -656,3 +690,18 @@ async def deactivate(self, *args: Any, **kwargs: Any) -> List[Event]:
"""Deactivates form."""
logger.debug(f"Deactivating the form '{self.name()}'")
return []

async def _activate_loop(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
events = self._default_activation_events()

temp_tracker = tracker.copy()
temp_tracker.update_with_events(events, domain)
events += await self.activate(output_channel, nlg, temp_tracker, domain)

return events
ancalita marked this conversation as resolved.
Show resolved Hide resolved
22 changes: 19 additions & 3 deletions rasa/core/actions/loops.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ async def run(
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
events = []
events: List[Event] = []

if not await self.is_activated(output_channel, nlg, tracker, domain):
events += self._default_activation_events()
events += await self.activate(output_channel, nlg, tracker, domain)
events += await self._activate_loop(
output_channel,
nlg,
tracker,
domain,
)

if not await self.is_done(output_channel, nlg, tracker, domain, events):
events += await self.do(output_channel, nlg, tracker, domain, events)
Expand Down Expand Up @@ -92,3 +96,15 @@ async def deactivate(
) -> List[Event]:
# can be overwritten
return []

async def _activate_loop(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
events = self._default_activation_events()
events += await self.activate(output_channel, nlg, tracker, domain)

return events
180 changes: 164 additions & 16 deletions tests/core/actions/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,59 @@ async def test_activate():
form_name = "my form"
action = FormAction(form_name, None)
slot_name = "num_people"
domain = f"""
slots:
{slot_name}:
type: float
mappings:
- type: from_entity
entity: number
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
domain = textwrap.dedent(
f"""
slots:
{slot_name}:
type: float
mappings:
- type: from_entity
entity: number
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
responses:
utter_ask_num_people:
- text: "How many people?"
"""
responses:
utter_ask_num_people:
- text: "How many people?"
"""
)
domain = Domain.from_yaml(domain)

events = await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events[:-1] == [ActiveLoop(form_name), SlotSet(REQUESTED_SLOT, slot_name)]
assert isinstance(events[-1], BotUttered)


async def test_activate_with_mapping_conditions_slot():
tracker = DialogueStateTracker.from_events(sender_id="bla", evts=[])
form_name = "my form"
action = FormAction(form_name, None)
slot_name = "num_people"
domain = textwrap.dedent(
f"""
slots:
{slot_name}:
type: float
mappings:
- type: from_entity
entity: number
conditions:
- active_loop: {form_name}
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
responses:
utter_ask_num_people:
- text: "How many people?"
"""
)
domain = Domain.from_yaml(domain)

events = await action.run(
Expand Down Expand Up @@ -108,6 +146,51 @@ async def test_activate_with_prefilled_slot():
]


async def test_activate_with_prefilled_slot_with_mapping_conditions():
slot_name = "num_people"
slot_value = 5

tracker = DialogueStateTracker.from_events(
sender_id="bla", evts=[SlotSet(slot_name, slot_value)]
)
form_name = "my form"
action = FormAction(form_name, None)

next_slot_to_request = "next slot to request"
domain = f"""
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
- {next_slot_to_request}
slots:
{slot_name}:
type: any
mappings:
- type: from_entity
entity: {slot_name}
conditions:
- active_loop: {form_name}
{next_slot_to_request}:
type: text
mappings:
- type: from_text
conditions:
- active_loop: {form_name}
ancalita marked this conversation as resolved.
Show resolved Hide resolved
"""
domain = Domain.from_yaml(domain)
events = await action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == [
ActiveLoop(form_name),
SlotSet(REQUESTED_SLOT, next_slot_to_request),
]


async def test_switch_forms_with_same_slot(default_agent: Agent):
"""Tests switching of forms, where the first slot is the same in both forms.

Expand Down Expand Up @@ -676,9 +759,9 @@ async def test_validate_slots_on_activation_with_other_action_after_user_utteran
)
tracker.update_with_events(slot_events, domain)

action = FormAction(form_name, action_server)
form_action = FormAction(form_name, action_server)

events = await action.run(
events = await form_action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
Expand Down Expand Up @@ -1671,3 +1754,68 @@ async def test_form_slots_empty_with_restart():
tracker,
domain,
)


async def test_extract_slots_with_mapping_conditions_during_form_activation():
slot_name = "city"
entity_value = "London"
entity_name = "location"

form_name = "test_form"

domain = Domain.from_yaml(
f"""
entities:
- {entity_name}
slots:
{slot_name}:
type: text
mappings:
- type: from_entity
entity: {entity_name}
conditions:
- active_loop: {form_name}
forms:
{form_name}:
{REQUIRED_SLOTS_KEY}:
- {slot_name}
"""
)

events = [
ActionExecuted("action_listen"),
UserUttered(
"I live in London",
entities=[{"entity": entity_name, "value": entity_value}],
),
]
tracker = DialogueStateTracker.from_events(
sender_id="test", evts=events, domain=domain, slots=domain.slots
)
assert tracker.active_loop_name is None

action_extract_slots = ActionExtractSlots(None)
events = await action_extract_slots.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)
assert events == []

expected_events = [
ActiveLoop(form_name),
SlotSet(slot_name, entity_value),
SlotSet(REQUESTED_SLOT, None),
ActiveLoop(None),
]

form_action = FormAction(form_name, None)
form_events = await form_action.run(
CollectingOutputChannel(),
TemplatedNaturalLanguageGenerator(domain.responses),
tracker,
domain,
)

assert form_events == expected_events