다음을 통해 공유


SharePoint Framework and Contextual Bots via Back Channel

This year Microsoft has made significant developer investments in SharePoint and bots, with new developer surfaces in the all-new SharePoint Framework and Bot Framework (respectively). Combining these technologies can deliver some very powerful scenarios. In fact, SharePoint PnP has a sample on embedding a bot into SharePoint. The sample does a good job of explaining the basics of the Bot Framework DirectLine channel and WebChat component (built with React). However, it really just shows how to embed a bot in SharePoint with no deeper integration. I imagine scenarios where the embedded bot automatically knows who the SharePoint user is and make REST calls on behalf of the user. In this post, I will demonstrate how a bot can interact and get contextual information from SharePoint through the Bot Framework "back channel".

[embed]https://www.youtube.com/watch?v=CLAQbDQwlrc[/embed]

Bot Architecture

To build a more contextual SharePoint/bot experience, it helps to understand the architecture of a bot built with the Bot Framework. Bot Framework bots use a REST endpoint that clients POST activity to. The activity type that is most obvious is a "Message". However, clients can pass additional activity types such as pings, typing, etc. The "back channel" involves posting activity to the same REST endpoint with the "Event" activity type. The "back channel" is bi-directional, so a bot endpoint can send "invisible" messages to a bot client by using the same "Event" activity type. Bot endpoints and bot clients just need to have additional logic to listen and respond to "Event" activity. This post will cover both.

Setup

Similar to the PnP sample, our back channel samples will leverage the Bot Framework WebChat control and the DirectLine channel of the Bot Framework. If you haven't used the Bot Framework before, you build a bot and then configure channels for that bot (ex: Skype, Microsoft Teams, Facebook, Slack, etc). DirectLine is a channel that allows more customized bot applications. You can learn more about it in the Bot Framework documentation or the PnP sample. I have checked in my SPFx samples with a DirectLine secret for a published bot...you are welcome to use this for testing. As a baseline, here is the code to leverage this control without any use of the back channel.

Bot Framework WebChat before back channel code

 import { App } from 'botframework-webchat';
import { DirectLine } from 'botframework-directlinejs';
require('../../../node_modules/BotFramework-WebChat/botchat.css');
import styles from './EchoBot.module.scss';
...
public render(): void {
   this.domElement.innerHTML = `<div id="${this.context.instanceId}" class="${styles.echobot}"></div>`;

   // Initialize DirectLine connection
   var botConnection = new DirectLine({
      secret: "AAos-s9yFEI.cwA.atA.qMoxsYRlWzZPgKBuo5ZfsRpASbo6XsER9i6gBOORIZ8"
   });

   // Initialize the BotChat.App with basic config data and the wrapper element
   App({
      user: { id: "Unknown", name: "Unknown" },
      botConnection: botConnection
   }, document.getElementById(this.context.instanceId));
}

Client to Bot Back Channel

I don't think all embedded bots need bi-directional use of the back channel. However, I do think all embedded bots can benefit from the client-to-bot direction, if only for contextual user/profile information. To use the back channel in this direction, the client needs call the postActivity method on the DirectLine botConnection with event data. Event data includes type ("event"), name (a unique name for your event), value (any data you want to send on the back channel), and from (the user object containing id and name). In the sample below, we are calling the SharePoint REST endpoint for profiles to retrieve the user's profile and sending their name through the back channel (using the event name "sendUserInfo").

Sending data from client to bot via back channel

 // Get userprofile from SharePoint REST endpoint
var req = new XMLHttpRequest();
req.open("GET", "/_api/SP.UserProfiles.PeopleManager/GetMyProperties", false);
req.setRequestHeader("Accept", "application/json");
req.send();
var user = { id: "userid", name: "unknown" };
if (req.status == 200) {
   var result = JSON.parse(req.responseText);
   user.id = result.Email;
   user.name = result.DisplayName;
}

// Initialize the BotChat.App with basic config data and the wrapper element
App({
   user: user,
   botConnection: botConnection
}, document.getElementById(this.context.instanceId));

// Call the bot backchannel to give it user information
botConnection
   .postActivity({ type: "event", name: "sendUserInfo", value: user.name, from: user })
   .subscribe(id => console.log("success")); 
}

On the bot endpoint, you need to listen for activity of type event. This will be slightly different depending on C# or Node bot implementation, but my sample uses C#. For C#, the activity type check can easily be implemented in the messages Web API (see here for Node example of back channel). Notice in the sample below we are extracting the user information sent through the back channel (on activity.Value) and saving it in UserState so it can be used throughout the conversation.

Using data sent through the back channel from client to bot

 public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
   if (activity.Type == ActivityTypes.Event && activity.Name == "sendUserInfo")
   {
      // Get the username from activity value then save it into BotState
      var username = activity.Value.ToString();
      var state = activity.GetStateClient();
      var userdata = state.BotState.GetUserData(activity.ChannelId, activity.From.Id);
      userdata.SetProperty<string>("username", username);
      state.BotState.SetUserData(activity.ChannelId, activity.From.Id, userdata);

      ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));
      Activity reply = activity.CreateReply($"The back channel has told me you are {username}. How cool is that!");
      await connector.Conversations.ReplyToActivityAsync(reply);
   }
   else if (activity.Type == ActivityTypes.Message)
   {
      // Handle actual messages coming from client
      // Removed for readability
   }
   var response = Request.CreateResponse(HttpStatusCode.OK);
   return response;
}

Bot to Client Back Channel

Sending data through the back channel from bot to client is as simple as sending a message. The only difference is you need to format the activity as an event with name and value. This is a little tricky in C# as you need to cast an IMessageActivity to IEventActivity and back (as seen below). The IEventActivity is new to BotBuilder, so you should update the Microsoft.Bot.Builder package to the latest (mine uses 3.5.2).

Sending data from bot to client via back channel

 [Serializable]
public class RootDialog : IDialog<IMessageActivity>
{
   public async Task StartAsync(IDialogContext context)
   {
      context.Wait(MessageReceivedAsync);
   }

   public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
   {
      var msg = await result;

      string[] options = new string[] { "Lists", "Webs", "ContentTypes"};
      var user = context.UserData.Get<string>("username");
      string prompt = $"Hey {user}, I'm a bot that can read your mind...well maybe not but I can count things in your SharePoint site. What do you want to count?";
      PromptDialog.Choice(context, async (IDialogContext choiceContext, IAwaitable<string> choiceResult) =>
      {
         var selection = await choiceResult;

         // Send the query through the backchannel using Event activity
         var reply = choiceContext.MakeMessage() as IEventActivity;
         reply.Type = "event";
         reply.Name = "runShptQuery";
         reply.Value = $"/_api/web/{selection}";
         await choiceContext.PostAsync((IMessageActivity)reply);
      }, options, prompt);
   }
}

Listening for the back channel events on the client again involves the DirectLine botConnection object where you filter and subscribe to specific activity. In the sample below we listen for activity type of event and name runShptQuery. When this type of activity is received, we perform a SharePoint REST query and return the aggregated results to the bot (again via back channel).

Using data sent through the back channel from bot to client

 // Listen for events on the backchannel
var act:any = botConnection.activity$;
act.filter(activity => activity.type == "event" && activity.name == "runShptQuery")
   .subscribe(a => {
      var activity:any = a;
      // Parse the entityType out of the value query string
      var entityType = activity.value.substr(activity.value.lastIndexOf("/") + 1);

      // Perform the REST call against SharePoint
      var shptReq = new XMLHttpRequest();
      shptReq.open("GET", activity.value, false);
      shptReq.setRequestHeader("Accept", "application/json");
      shptReq.send();
      var shptResult = JSON.parse(shptReq.responseText);

      // Call the bot backchannel to give the aggregated results
      botConnection
        .postActivity({ type: "event", name: "queryResults", value: { entityType: entityType, count: shptResult.value.length }, from: user })
        .subscribe(id => console.log("success sending results"));
   });

Conclusion

Hopefully you can see how much more powerful a SharePoint or Office embedded bot can become with additional context provided through the back channel. I'm really excited to see what creative solutions developers can come up with this, so keep me posted. Big props to Bill Barnes and Ryan Volum on my team for their awesome work on the WebChat and the back channel. Below, I have listed four repositories used in this post. I have purposefully checked in the SharePoint Framework projects with a DirectLine secret to my bot so you can immediately run them without deploying your own bot.

SPFx-1-Way-Bot-Back-Channel
Simple SharePoint Framework Project that embeds a bot and uses the Bot Framework back channel to silently send the bot contextual information about the user.

SPFx-2-Way-Bot-Back-Channel
Simple SharePoint Framework Project that embeds a bot and uses the Bot Framework back channel to silently integrate the bot and client to share contextual information and API calls.

CSharp-BotFramework-OneWay-BackChannel
Simple C# Bot Framework project that listens on the back channel for contextual information sent from the client (WebChat client in SharePoint page)

CSharp-BotFramework-TwoWay-BackChannel
Simple C# Bot Framework project that uses the back channel for contextual information and API calls against the client (WebChat client in SharePoint page)