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.