Sharing my solution: Using channels to support multiple bots using one Rasa Core instance

I had the use case where I had different chatbots that I wanted responding differently to the user. One way to do this was to create different models and deploy multiple Rasa Core instances.

This seemed too much overhead for me, and in my case, all the chatbots would respond to similar intents (they are characters in a game).

I found that custom channel connectors lets me send different responses to different channels (Custom Connectors) and wrote this connector which does exactly what I needed.

Here it is if you have a similar use case:

import asyncio
import inspect
from sanic import Sanic, Blueprint, response
from sanic.request import Request
from sanic.response import HTTPResponse
from typing import Text, Dict, Any, Optional, Callable, Awaitable, NoReturn
from rasa.core.actions.action import ActionRetrieveResponse
from rasa.core.channels.channel import (
    InputChannel,
    CollectingOutputChannel,
    UserMessage,
)


class OpenConversationOutputChannel(CollectingOutputChannel):
    async def send_response(self, recipient_id: Text, message: Dict[Text, Any]) -> None:
        """Send a message to the client."""
        if message.get("text"):
            await self.send_text_message(
                recipient_id,
                message.pop("text"),
                message.pop("utter_action"),  # Include the utter_action
                **message,
            )
        else:
            super().send_response(recipient_id, message)

    async def send_text_message(
        self, recipient_id: Text, text: Text, utter_action: Text, **kwargs: Any
    ) -> None:
        for message_part in text.strip().split("\n\n"):
            await self._persist_message(
                self._message(
                    recipient_id,
                    text=message_part,
                    custom={
                        "utter_action": utter_action,
                        "intent": ActionRetrieveResponse.intent_name_from_action(
                            utter_action
                        ),
                    },
                )
            )


class AlexOutputChannel(OpenConversationOutputChannel):
    @classmethod
    def name(cls) -> Text:
        return "alex"


class AliceOutputChannel(OpenConversationOutputChannel):
    @classmethod
    def name(cls) -> Text:
        return "alice"


class ClaraOutputChannel(OpenConversationOutputChannel):
    @classmethod
    def name(cls) -> Text:
        return "clara"


class OpenConversationChannel(InputChannel):
    _character_name_to_channel: Dict[str, CollectingOutputChannel] = {
        "alex": AlexOutputChannel(),
        "alice": AliceOutputChannel(),
        "clara": ClaraOutputChannel(),
    }

    def name(self) -> Text:
        """Name of your custom channel."""
        return "openconversation"

    def get_metadata(self, request: Request) -> Optional[Dict[Text, Any]]:
        return request.json.get("metadata")

    def get_character_name(self, request: Request) -> Optional[str]:
        metadata = self.get_metadata(request)

        if metadata:
            return metadata.get("character")
        else:
            return None

    def blueprint(
        self, on_new_message: Callable[[UserMessage], Awaitable[None]]
    ) -> Blueprint:

        custom_webhook = Blueprint(
            "custom_webhook_{}".format(type(self).__name__),
            inspect.getmodule(self).__name__,
        )

        @custom_webhook.route("/", methods=["GET"])
        async def health(request: Request) -> HTTPResponse:
            return response.json({"status": "ok"})

        @custom_webhook.route("/webhook", methods=["POST"])
        async def receive(request: Request) -> HTTPResponse:
            sender_id = request.json.get("sender")  # method to get sender_id
            text = request.json.get("message")  # method to fetch text
            input_channel = self.name()  # method to fetch input channel
            metadata = self.get_metadata(request)

            # method to get character name
            character_name = self.get_character_name(request)

            collector = (
                self._character_name_to_channel.get(character_name)
                or CollectingOutputChannel()
            )

            await on_new_message(
                UserMessage(
                    text,
                    collector,
                    sender_id,
                    input_channel=input_channel,
                    metadata=metadata,
                )
            )

            return response.json(collector.messages)

        return custom_webhook

Then make sure you add the channel info to your NLU. See the above link for more details.

Now, if you send the following request to http://localhost:5005/webhooks/openconversation/webhook:

{
    "sender": "test_user",
    "message": "who are you?",
    "metadata": {
        "character": "alice"
    }
}

You’d get the appropriate response.

There is additional code above that exposes the detected “intent” and “action” and passes it in the response. I needed that for my use case but feel free to delete the send_response and send_text_message overrides if you don’t need them.

2 Likes