A bot build on Microsoft Bot Framework that routes messages between two users on different channels. This is sample utilizes the core functionality found in Bot Message Routing (component) project.
This is a C# sample - if you're looking to do this with Node, see this sample.
A possible use case for this type of a bot would be a customer service scenario where the bot relays the messages between a customer and a customer service agent.
To test the bot, publish it in Microsoft Bot Framework portal and connect it to the channels of your choice. If you are new to bots, please familiarize yourself first with the basics here. Microsoft Bot Framework Emulator is a great tool for testing and debugging - you can download it from here. To communicate with a remotely hosted bot, you should use ngrok tunneling software:
- In emulator open App Settings
- Make sure ngrok path is set:
- See the emulator log to verify the path was set correctly:
- Set the bot end point in emulator (
https://<bot URL>/api/messages
) - Set Microsoft App ID and Microsoft App Password
- Click CONNECT to start a new conversation
See also: Microsoft Bot Framework Emulator wiki
This scenario utilizes an aggregation concept (see the terminology table in this document). One or more channels act as aggregated channels where the customer requests (for human assistance) are sent. The conversation owners (e.g. customer service agents) then accept or reject the requests.
Once you have published the bot, go to the channel you want to receive the requests and issue the following command to the bot (given that you haven't changed the default bot command handler or the command itself):
@<bot name> watch
In case mentions are not supported, you can also use the command keyword:
command watch
Now all the requests from another channels are forwarded to this channel. See the default flow below:
Emulator with ngrok | Slack |
---|---|
![]() |
|
![]() |
![]() |
![]() |
|
![]() |
![]() |
In this scenario the conversation owners (e.g. customer service agents) access the bot via the webchat component, Agent UI, implemented by Bill Barnes. Each customer request (for human assistance) automatically opens a new chat window in the agent UI.
Emulator | Agent UI |
---|---|
![]() |
![]() |
To set this up, follow these steps:
-
Make sure you have Node.js installed
-
Clone or download the Agent UI repository
-
Inside
index.ts
, update the line below with your bot's endpoint:fetch("http://YOUR_BOT_ENDPOINT/api/agent/1")
Example:
fetch("http://mybot.azurewebsites.net/api/agent/1")
-
Inside
index.ts
, update the line below with your bot secret keyiframe.src = 'botchat?s=YOUR_DIRECTLINE_SECRET_ID';
- The bot secret key can be found in your bot's profile in the portal
- Click on the Edit button next to the Direct Line channel to locate the secret key
- If your Configure Direct Line page is blank, create a new site by clicking Add new site and two bot secret keys will be generated for you:
-
Run
npm install
to get the npm packages- You only need to run this command once, unless you add other node packages to the project
- If you encounter
error TS2300
, runnpm install [email protected]
-
Run
npm run build
to build the app- You need to run this every time you make changes to the code before you start the application
-
Run
npm run start
to start the app -
Go to http://localhost:8080 to see the Agent UI
Make sure that the value of RejectPendingRequestIfNoAggregationChannel
key in
Web.config is false
:
<add key="<add key="RejectPendingRequestIfNoAggregationChannel" value="false" />" value="false" />
Otherwise the agent UI will not receive the requests, but they are automatically rejected (if no aggregation channel is set).
The bot comes with a CommandMessageHandler class, which implements the commands in the table below.
Command | Description |
---|---|
options |
Displays the command options as a card with buttons (convenient!) |
watch |
Marks the current channel as aggregation channel (where requests are sent). |
accept <user ID> |
Accepts the conversation connection request of the given user. |
reject <user ID> |
Rejects the conversation connection request of the given user. |
disconnect |
Ends the current conversation with a user. |
reset |
Deletes all routing data! |
list parties |
Lists all parties the bot is aware of. |
list requests |
Lists all pending requests. |
list conversations |
Lists all conversations (connections). |
list results |
Lists all handled results (MessageRouterResult ). |
To issue a command use the bot name:
@<bot name> <command> <optional parameters>
In case mentions are not supported, you can also use the command keyword:
command <command> <optional parameters>
The core message routing functionality comes from the Bot Message Routing (component) project. This sample demonstrates how to use the component and provides the necessary "plumbing" such as command handling.
The key classes of this sample are:
-
AgentController: A controller for the agent UI. Enables the agent UI to check the status of pending requests and automatically accept them.
-
BackChannelMessageHandler: Provides implementation for checking and acting on back channel (command) messages. Back channel messages are used by the agent UI.
-
CommandMessageHandler: Provides implementation for checking and acting on commands in messages before they are passed to a dialog etc.
-
MessageRouterResultHandler: Implements
IMessageRouterResultHandler
. Handles the results of the operations executed byMessageRouterManager
.
See also: Taking the code into use
A number of app settings are available in the Web.config file of this sample which can be used to tailor the experience.
PermittedAgentChannels: If you wish to only allow conversation owners (i.e. customer service agent) to use a specific channel or channels, you can specify a comma seperated list of channel IDs here. This will prevent agent commands from being used on other channels and prevent users from accidentally or deliberately calling such commands. E.g. to allow agents to use the emulator and Skype channels you would use.
<add key="PermittedAgentChannels" value="emulator,skype" />
RejectPendingRequestIfNoAggregationChannel: This setting, which is set to true by default, will
cause the LocalRoutingDataManager to return the NoAgentsAvailable result when no agents are watching
for incoming requests. You can then send an appropriate response to let the user know no agents are
available within the implementation of IMessageRouterResultHandler
. If this is set to false, then
the LocalRoutingDataManager
will process and add the users request to the pending requests list
and return the ConnectionRequested
result instead.
The most convenient place to use the aforementioned classes is in the
MessagesController
class - you can first call the methods in MessageRouterManager
and, for instance, if no action is
taken by the manager, you can forward the Activity
to a Dialog
:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
MessageRouterManager messageRouterManager = WebApiConfig.MessageRouterManager;
IMessageRouterResultHandler messageRouterResultHandler = WebApiConfig.MessageRouterResultHandler;
messageRouterManager.MakeSurePartiesAreTracked(activity);
// First check for commands (both from back channel and the ones directly typed)
MessageRouterResult messageRouterResult =
WebApiConfig.BackChannelMessageHandler.HandleBackChannelMessage(activity);
if (messageRouterResult.Type != MessageRouterResultType.Connected
&& await WebApiConfig.CommandMessageHandler.HandleCommandAsync(activity) == false)
{
// No valid back channel (command) message or typed command detected
// Let the message router manager instance handle the activity
messageRouterResult = await messageRouterManager.HandleActivityAsync(activity, false);
if (messageRouterResult.Type == MessageRouterResultType.NoActionTaken)
{
// No action was taken by the message router manager. This means that the
// user is not connected (in a 1:1 conversation) with a human
// (e.g. customer service agent) yet.
//
// You can, for example, check if the user (customer) needs human
// assistance here or forward the activity to a dialog. You could also do
// the check in the dialog too...
//
// Here's an example:
if (!string.IsNullOrEmpty(activity.Text)
&& activity.Text.ToLower().Contains(CommandRequestConnection))
{
messageRouterResult = messageRouterManager.RequestConnection(activity);
}
else
{
await Conversation.SendAsync(activity, () => new RootDialog());
}
}
}
// Handle the result, if required
await messageRouterResultHandler.HandleResultAsync(messageRouterResult);
}
else
{
await HandleSystemMessageAsync(activity);
}
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}