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

Eng 545 stack cleaning #12834

Merged
merged 21 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
837eaac
improved typing of FlowLink
twerkmeister Sep 20, 2023
c7c9947
consistently using kwargs
twerkmeister Sep 20, 2023
f87bf7e
implemented stack cleaning and test
twerkmeister Sep 21, 2023
f7a4391
Revert "improved typing of FlowLink"
twerkmeister Sep 22, 2023
a0b95b3
improved and fixed tests relying on strict number indices
twerkmeister Sep 25, 2023
cba9791
Only update flow hashes when they changed, added slot to tests
twerkmeister Sep 25, 2023
75bad36
Code quality, moved fixtures to conftest
twerkmeister Sep 25, 2023
2cf529a
removed unused imports
twerkmeister Sep 25, 2023
416be84
Added explicit typing
twerkmeister Sep 25, 2023
68af0a3
improved tests
twerkmeister Sep 25, 2023
cc7e1a0
removed unused imports
twerkmeister Sep 25, 2023
64de8f1
Merge branch 'dm2' into ENG-545-stack-cleaning
tmbo Sep 27, 2023
b03c781
Update rasa/dialogue_understanding/patterns/default_flows_for_pattern…
twerkmeister Sep 28, 2023
17d9ff6
Update rasa/dialogue_understanding/patterns/clean_stack.py
twerkmeister Sep 28, 2023
c9fff34
applying and returning flow hash events
twerkmeister Sep 28, 2023
e17c603
added logging call for when flows were updated
twerkmeister Sep 28, 2023
d7857d0
made fingerprint of flows a cached property
twerkmeister Sep 28, 2023
7a629f9
restructured command, pattern, and action to be more modular
twerkmeister Sep 28, 2023
586e1c7
adjusted flow texts to reflect conversation designer preferences
twerkmeister Sep 28, 2023
a46c520
fixes for code quality, tests
twerkmeister Sep 28, 2023
ff6c1fb
fixed default flow syntax
twerkmeister Sep 29, 2023
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 data/test_trackers/tracker_moodbot.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"followup_action": null,
"slots": {
"dialogue_stack": null,
"flow_hashes": null,
"name": null,
"requested_slot": null,
"return_value": null,
Expand Down
5 changes: 4 additions & 1 deletion rasa/core/actions/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
ACTION_VALIDATE_SLOT_MAPPINGS,
MAPPING_TYPE,
SlotMappingType,
KNOWLEDGE_BASE_SLOT_NAMES,
)
from rasa.shared.core.domain import Domain
from rasa.shared.core.events import (
Expand Down Expand Up @@ -1292,7 +1293,9 @@ async def run(
executed_custom_actions: Set[Text] = set()

user_slots = [
slot for slot in domain.slots if slot.name not in DEFAULT_SLOT_NAMES
slot
for slot in domain.slots
if slot.name not in DEFAULT_SLOT_NAMES | KNOWLEDGE_BASE_SLOT_NAMES
]

for slot in user_slots:
Expand Down
4 changes: 2 additions & 2 deletions rasa/dialogue_understanding/commands/cancel_flow_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ def select_canceled_frames(stack: DialogueStack) -> List[str]:
The frames that were canceled."""
canceled_frames = []
# we need to go through the original stack dump in reverse order
# to find the frames that were canceled. we cancel everthing from
# to find the frames that were canceled. we cancel everything from
# the top of the stack until we hit the user flow that was canceled.
# this will also cancel any patterns put ontop of that user flow,
# this will also cancel any patterns put on top of that user flow,
# e.g. corrections.
for frame in reversed(stack.frames):
canceled_frames.append(frame.frame_id)
Expand Down
67 changes: 67 additions & 0 deletions rasa/dialogue_understanding/commands/clean_stack_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, List

import structlog

from rasa.dialogue_understanding.commands import Command
from rasa.dialogue_understanding.patterns.clean_stack import CleanStackFlowStackFrame
from rasa.dialogue_understanding.stack.dialogue_stack import DialogueStack
from rasa.shared.core.constants import DIALOGUE_STACK_SLOT
from rasa.shared.core.events import Event, SlotSet
from rasa.shared.core.flows.flow import FlowsList
from rasa.shared.core.trackers import DialogueStateTracker
from rasa.dialogue_understanding.stack.utils import top_user_flow_frame

structlogger = structlog.get_logger()


@dataclass
class CleanStackCommand(Command):
"""A command to cancel the current flow."""

@classmethod
def command(cls) -> str:
"""Returns the command type."""
return "clean stack"

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> CleanStackCommand:
"""Converts the dictionary to a command.

Returns:
The converted dictionary.
"""
return CleanStackCommand()

def run_command_on_tracker(
self,
tracker: DialogueStateTracker,
all_flows: FlowsList,
original_tracker: DialogueStateTracker,
) -> List[Event]:
"""Runs the command on the tracker.

Args:
tracker: The tracker to run the command on.
all_flows: All flows in the assistant.
original_tracker: The tracker before any command was executed.

Returns:
The events to apply to the tracker.
"""

stack = DialogueStack.from_tracker(tracker)
original_stack = DialogueStack.from_tracker(original_tracker)
user_frame = top_user_flow_frame(original_stack)
current_flow = user_frame.flow(all_flows) if user_frame else None

if not current_flow:
structlogger.debug(
"command_executor.skip_clean_stack.no_active_flow", command=self
)
return []

stack.push(CleanStackFlowStackFrame())
return [SlotSet(DIALOGUE_STACK_SLOT, stack.as_dict())]
2 changes: 1 addition & 1 deletion rasa/dialogue_understanding/patterns/cancel.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def from_dict(data: Dict[str, Any]) -> CancelPatternFlowStackFrame:
The created `DialogueStackFrame`.
"""
return CancelPatternFlowStackFrame(
data["frame_id"],
frame_id=data["frame_id"],
step_id=data["step_id"],
canceled_name=data["canceled_name"],
canceled_frames=data["canceled_frames"],
Expand Down
2 changes: 1 addition & 1 deletion rasa/dialogue_understanding/patterns/clarify.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def from_dict(data: Dict[str, Any]) -> ClarifyPatternFlowStackFrame:
The created `DialogueStackFrame`.
"""
return ClarifyPatternFlowStackFrame(
data["frame_id"],
frame_id=data["frame_id"],
step_id=data["step_id"],
names=data["names"],
clarification_options=data["clarification_options"],
Expand Down
96 changes: 96 additions & 0 deletions rasa/dialogue_understanding/patterns/clean_stack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, List, Optional

import structlog
from rasa.dialogue_understanding.stack.dialogue_stack import (
DialogueStack,
)
from rasa.dialogue_understanding.stack.frames import (
PatternFlowStackFrame,
BaseFlowStackFrame,
UserFlowStackFrame,
)
from rasa.core.actions import action
from rasa.core.channels.channel import OutputChannel
from rasa.core.nlg.generator import NaturalLanguageGenerator
from rasa.dialogue_understanding.stack.frames.flow_stack_frame import FlowStackFrameType
from rasa.shared.constants import RASA_DEFAULT_FLOW_PATTERN_PREFIX
from rasa.shared.core.constants import DIALOGUE_STACK_SLOT, ACTION_CLEAN_STACK
from rasa.shared.core.domain import Domain
from rasa.shared.core.events import Event, SlotSet
from rasa.shared.core.flows.flow import END_STEP, ContinueFlowStep
from rasa.shared.core.trackers import DialogueStateTracker


structlogger = structlog.get_logger()

FLOW_PATTERN_CLEAN_STACK_ID = RASA_DEFAULT_FLOW_PATTERN_PREFIX + "clean_stack"


@dataclass
class CleanStackFlowStackFrame(PatternFlowStackFrame):
"""A pattern flow stack frame which cleans the stack after a bot update."""

flow_id: str = FLOW_PATTERN_CLEAN_STACK_ID
"""The ID of the flow."""

@classmethod
def type(cls) -> str:
"""Returns the type of the frame."""
return "pattern_clean_stack"
twerkmeister marked this conversation as resolved.
Show resolved Hide resolved

@staticmethod
def from_dict(data: Dict[str, Any]) -> CleanStackFlowStackFrame:
"""Creates a `DialogueStackFrame` from a dictionary.

Args:
data: The dictionary to create the `DialogueStackFrame` from.

Returns:
The created `DialogueStackFrame`.
"""
return CleanStackFlowStackFrame(
frame_id=data["frame_id"],
step_id=data["step_id"],
)


class ActionCleanStack(action.Action):
"""Action which cancels a flow from the stack."""

def name(self) -> str:
"""Return the flow name."""
return ACTION_CLEAN_STACK

async def run(
self,
output_channel: OutputChannel,
nlg: NaturalLanguageGenerator,
tracker: DialogueStateTracker,
domain: Domain,
metadata: Optional[Dict[str, Any]] = None,
) -> List[Event]:
"""Clean the stack."""
stack = DialogueStack.from_tracker(tracker)
if not (top := stack.top()):
structlogger.warning("action.clean_stack.no_active_flow")
return []

if not isinstance(top, CleanStackFlowStackFrame):
structlogger.warning("action.clean_stack.no_cleaning_frame", top=top)
return []

new_frames = []
# Set all frames to their end step, filter out any non-BaseFlowStackFrames
for frame in stack.frames:
if isinstance(frame, BaseFlowStackFrame):
frame.step_id = ContinueFlowStep.continue_step_for_id(END_STEP)
if isinstance(frame, UserFlowStackFrame):
# Making sure there are no "continue interrupts" triggered
frame.frame_type = FlowStackFrameType.REGULAR
new_frames.append(frame)
new_stack = DialogueStack.from_dict([frame.as_dict() for frame in new_frames])

return [SlotSet(DIALOGUE_STACK_SLOT, new_stack.as_dict())]
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def from_dict(data: Dict[str, Any]) -> CollectInformationPatternFlowStackFrame:
]

return CollectInformationPatternFlowStackFrame(
data["frame_id"],
frame_id=data["frame_id"],
step_id=data["step_id"],
collect=data["collect"],
utter=data["utter"],
Expand Down
2 changes: 1 addition & 1 deletion rasa/dialogue_understanding/patterns/completed.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def from_dict(data: Dict[str, Any]) -> CompletedPatternFlowStackFrame:
The created `DialogueStackFrame`.
"""
return CompletedPatternFlowStackFrame(
data["frame_id"],
frame_id=data["frame_id"],
step_id=data["step_id"],
previous_flow_name=data["previous_flow_name"],
)
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def from_dict(data: Dict[str, Any]) -> ContinueInterruptedPatternFlowStackFrame:
The created `DialogueStackFrame`.
"""
return ContinueInterruptedPatternFlowStackFrame(
data["frame_id"],
frame_id=data["frame_id"],
step_id=data["step_id"],
previous_flow_name=data["previous_flow_name"],
)
2 changes: 1 addition & 1 deletion rasa/dialogue_understanding/patterns/correction.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def from_dict(data: Dict[Text, Any]) -> CorrectionPatternFlowStackFrame:
The created `DialogueStackFrame`.
"""
return CorrectionPatternFlowStackFrame(
data["frame_id"],
frame_id=data["frame_id"],
step_id=data["step_id"],
is_reset_only=data["is_reset_only"],
corrected_slots=data["corrected_slots"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ responses:
rephrase: True
template: jinja

utter_inform_stack_cleanup_rasa:
- text: There has been an update to my code. I need to wrap up our running dialogue tasks and start from scratch.
twerkmeister marked this conversation as resolved.
Show resolved Hide resolved
metadata:
rephrase: True

slots:
confirm_correction:
Expand Down Expand Up @@ -125,3 +129,12 @@ flows:
action: action_listen
next: "start"
- id: "done"

pattern_clean_stack:
description: flow used to clean the stack after a bot update
steps:
- id: "inform_user"
action: utter_inform_stack_cleanup
next: "run_cleanup"
- id: "run_cleanup"
action: action_clean_stack
twerkmeister marked this conversation as resolved.
Show resolved Hide resolved
47 changes: 46 additions & 1 deletion rasa/dialogue_understanding/processor/command_processor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Optional, Type
from typing import List, Optional, Type, Set, Dict

import structlog
from rasa.dialogue_understanding.commands import (
Expand All @@ -9,6 +9,7 @@
SetSlotCommand,
FreeFormAnswerCommand,
)
from rasa.dialogue_understanding.commands.clean_stack_command import CleanStackCommand
from rasa.dialogue_understanding.patterns.collect_information import (
CollectInformationPatternFlowStackFrame,
)
Expand All @@ -23,6 +24,7 @@
filled_slots_for_active_flow,
top_flow_frame,
)
from rasa.shared.core.constants import FLOW_HASHES_SLOT
from rasa.shared.core.events import Event, SlotSet
from rasa.shared.core.flows.flow import (
FlowsList,
Expand Down Expand Up @@ -95,6 +97,39 @@ def validate_state_of_commands(commands: List[Command]) -> None:
assert sum(isinstance(c, CorrectSlotsCommand) for c in commands) <= 1


def find_updated_flows(tracker: DialogueStateTracker, all_flows: FlowsList) -> Set[str]:
"""Find the set of updated flows.

Run through the current dialogue stack and compare the flow hashes of the
flows on the stack with those stored in the tracker.

Args:
tracker: The tracker.
all_flows: All flows.

Returns:
A set of flow ids of those flows that have changed
"""
stored_fingerprints: Dict[str, str] = tracker.get_slot(FLOW_HASHES_SLOT) or {}
dialogue_stack = DialogueStack.from_tracker(tracker)

changed_flows = set()
for frame in dialogue_stack.frames:
if isinstance(frame, BaseFlowStackFrame):
flow = all_flows.flow_by_id(frame.flow_id)
if flow is None or (
flow.id in stored_fingerprints
and flow.fingerprint() != stored_fingerprints[flow.id]
):
changed_flows.add(frame.flow_id)
return changed_flows


def calculate_flow_fingerprints(all_flows: FlowsList) -> Dict[str, str]:
"""Calculate fingerprints for all flows."""
return {flow.id: flow.fingerprint() for flow in all_flows.underlying_flows}


def execute_commands(
tracker: DialogueStateTracker, all_flows: FlowsList
) -> List[Event]:
Expand All @@ -113,6 +148,16 @@ def execute_commands(

commands = clean_up_commands(commands, tracker, all_flows)

updated_flows = find_updated_flows(tracker, all_flows)
if updated_flows:
# Override commands
commands = [CleanStackCommand()]
twerkmeister marked this conversation as resolved.
Show resolved Hide resolved

# store current flow hashes if they changed
new_hashes = calculate_flow_fingerprints(all_flows)
if new_hashes != (tracker.get_slot(FLOW_HASHES_SLOT) or {}):
tracker.update_with_events([SlotSet(FLOW_HASHES_SLOT, new_hashes)], None)

twerkmeister marked this conversation as resolved.
Show resolved Hide resolved
events: List[Event] = []

# commands need to be reversed to make sure they end up in the right order
Expand Down
8 changes: 4 additions & 4 deletions rasa/dialogue_understanding/stack/frames/flow_stack_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ def from_dict(data: Dict[str, Any]) -> UserFlowStackFrame:
The created `DialogueStackFrame`.
"""
return UserFlowStackFrame(
data["frame_id"],
data["flow_id"],
data["step_id"],
FlowStackFrameType.from_str(data.get("frame_type")),
frame_id=data["frame_id"],
flow_id=data["flow_id"],
step_id=data["step_id"],
frame_type=FlowStackFrameType.from_str(data.get("frame_type")),
)
Loading
Loading