Core: 3. Responses#

This tutorial shows different options for setting responses.

Here, responses that allow giving custom answers to users are shown.

[1]:
# installing dependencies
%pip install -q chatsky
Note: you may need to restart the kernel to use updated packages.
[2]:
import re
import random
from typing import Union

from chatsky import (
    TRANSITIONS,
    RESPONSE,
    Context,
    Message,
    Pipeline,
    Transition as Tr,
    conditions as cnd,
    responses as rsp,
    destinations as dst,
    BaseResponse,
    MessageInitTypes,
    AnyResponse,
    AbsoluteNodeLabel,
)

from chatsky.utils.testing.common import (
    check_happy_path,
    is_interactive_mode,
)

Response of a node is determined by BaseResponse.

Response can be constant in which case it is an instance of Message.

Message has an option to be instantiated from a string which is what we’ve been using so far. Under the hood RESPONSE: "text" is converted into RESPONSE: Message(text="text"). This class should be used over simple strings when some additional information needs to be sent such as images/metadata.

More information on that can be found in the media tutorial.

Instances of this class are returned by last_request and last_response. In the previous tutorial we showed how to access fields of messages to build custom conditions.

Node RESPONSE can also be set to a custom function. This is demonstrated below:

[3]:
class CannotTalkAboutTopic(BaseResponse):
    async def call(self, ctx: Context) -> MessageInitTypes:
        request = ctx.last_request
        if request.text is None:
            topic = None
        else:
            topic_pattern = re.compile(r"(.*talk about )(.*)\.")
            topic = topic_pattern.findall(request.text)
            topic = topic and topic[0] and topic[0][-1]
        if topic:
            return f"Sorry, I can not talk about {topic} now."
        else:
            return "Sorry, I can not talk about that now."


class UpperCase(BaseResponse):
    response: AnyResponse  # either const response or another BaseResponse

    def __init__(self, response: Union[MessageInitTypes, BaseResponse]):
        # defining this allows passing response as a positional argument
        # and allows to make a more detailed type annotation:
        # AnyResponse cannot be a string but can be initialized from it,
        # so MessageInitTypes annotates that we can init from a string
        super().__init__(response=response)

    async def call(self, ctx: Context) -> MessageInitTypes:
        response = await self.response(ctx)
        # const response is converted to BaseResponse,
        # so we call it regardless of the response type

        if response.text is not None:
            response.text = response.text.upper()
        return response


class FallbackTrace(BaseResponse):
    async def call(self, ctx: Context) -> MessageInitTypes:
        return Message(
            misc={
                "previous_node": await dst.Previous()(ctx),
                "last_request": ctx.last_request,
            }
        )

Chatsky provides one basic response as part of the standard module:

  • RandomChoice randomly chooses a message out of the ones passed to it.

[4]:
toy_script = {
    "greeting_flow": {
        "start_node": {
            TRANSITIONS: [Tr(dst="node1", cnd=cnd.ExactMatch("Hi"))],
        },
        "node1": {
            RESPONSE: rsp.RandomChoice(
                "Hi, what is up?",
                "Hello, how are you?",
            ),
            # Random choice from candidate list.
            TRANSITIONS: [
                Tr(dst="node2", cnd=cnd.ExactMatch("I'm fine, how are you?"))
            ],
        },
        "node2": {
            RESPONSE: "Good. What do you want to talk about?",
            TRANSITIONS: [
                Tr(dst="node3", cnd=cnd.ExactMatch("Let's talk about music."))
            ],
        },
        "node3": {
            RESPONSE: CannotTalkAboutTopic(),
            TRANSITIONS: [Tr(dst="node4", cnd=cnd.ExactMatch("Ok, goodbye."))],
        },
        "node4": {
            RESPONSE: UpperCase("bye"),
            TRANSITIONS: [Tr(dst="node1", cnd=cnd.ExactMatch("Hi"))],
        },
        "fallback_node": {
            RESPONSE: FallbackTrace(),
            TRANSITIONS: [Tr(dst="node1", cnd=cnd.ExactMatch("Hi"))],
        },
    }
}

# testing
happy_path = (
    (
        "Hi",
        "Hello, how are you?",
    ),  # start_node -> node1
    (
        "I'm fine, how are you?",
        "Good. What do you want to talk about?",
    ),  # node1 -> node2
    (
        "Let's talk about music.",
        "Sorry, I can not talk about music now.",
    ),  # node2 -> node3
    ("Ok, goodbye.", "BYE"),  # node3 -> node4
    ("Hi", "Hello, how are you?"),  # node4 -> node1
    (
        "stop",
        Message(
            misc={
                "previous_node": AbsoluteNodeLabel(
                    flow_name="greeting_flow", node_name="node1"
                ),
                "last_request": Message("stop"),
            }
        ),
    ),
    # node1 -> fallback_node
    (
        "one",
        Message(
            misc={
                "previous_node": AbsoluteNodeLabel(
                    flow_name="greeting_flow", node_name="fallback_node"
                ),
                "last_request": Message("one"),
            }
        ),
    ),  # f_n->f_n
    (
        "help",
        Message(
            misc={
                "previous_node": AbsoluteNodeLabel(
                    flow_name="greeting_flow", node_name="fallback_node"
                ),
                "last_request": Message("help"),
            }
        ),
    ),  # f_n->f_n
    (
        "nope",
        Message(
            misc={
                "previous_node": AbsoluteNodeLabel(
                    flow_name="greeting_flow", node_name="fallback_node"
                ),
                "last_request": Message("nope"),
            }
        ),
    ),  # f_n->f_n
    (
        "Hi",
        "Hi, what is up?",
    ),  # fallback_node -> node1
    (
        "I'm fine, how are you?",
        "Good. What do you want to talk about?",
    ),  # node1 -> node2
    (
        "Let's talk about music.",
        "Sorry, I can not talk about music now.",
    ),  # node2 -> node3
    ("Ok, goodbye.", "BYE"),  # node3 -> node4
)
[5]:
random.seed(31415)  # predestination of choice


pipeline = Pipeline(
    script=toy_script,
    start_label=("greeting_flow", "start_node"),
    fallback_label=("greeting_flow", "fallback_node"),
)

if __name__ == "__main__":
    check_happy_path(pipeline, happy_path, printout=True)
    if is_interactive_mode():
        pipeline.run()
USER: text='Hi'
BOT : text='Hello, how are you?'
USER: text='I'm fine, how are you?'
BOT : text='Good. What do you want to talk about?'
USER: text='Let's talk about music.'
BOT : text='Sorry, I can not talk about music now.'
USER: text='Ok, goodbye.'
BOT : text='BYE'
USER: text='Hi'
BOT : text='Hello, how are you?'
USER: text='stop'
BOT : misc='{'previous_node': {'flow_name': 'greeting_flow', 'node_name': 'node1'}, 'last_request': {'text': 'stop'}}'
USER: text='one'
BOT : misc='{'previous_node': {'flow_name': 'greeting_flow', 'node_name': 'fallback_node'}, 'last_request': {'text': 'one'}}'
USER: text='help'
BOT : misc='{'previous_node': {'flow_name': 'greeting_flow', 'node_name': 'fallback_node'}, 'last_request': {'text': 'help'}}'
USER: text='nope'
BOT : misc='{'previous_node': {'flow_name': 'greeting_flow', 'node_name': 'fallback_node'}, 'last_request': {'text': 'nope'}}'
USER: text='Hi'
BOT : text='Hi, what is up?'
USER: text='I'm fine, how are you?'
BOT : text='Good. What do you want to talk about?'
USER: text='Let's talk about music.'
BOT : text='Sorry, I can not talk about music now.'
USER: text='Ok, goodbye.'
BOT : text='BYE'