Download / Source code

SPFX webpart : https://github.com/get2pallav/BT/tree/develop

BOT Code : https://github.com/get2pallav/BOT_FM

Introduction

The Microsoft Bot Framework provides just what we need to build and connect intelligent bots that interact naturally wherever users are talking, from text/sms to Skype, Slack, Office 365 mail and other popular services. Within the Bot Framework, the Bot Connector service enables our bot to exchange messages with users on channels that are configured in the Bot Framework Portal and the Bot State service enables bot to store and retrieve state data that is related to the conversations that our bot conducts using the Bot Connector. 

Here we will explore how to include Microsoft BOT Framework and use its capabilities with SharePoint using SPFx webpart. This will be a simple BOT which will using SharePoint context to fetch user information and interact with the user in natural language.  

BOT Registration

The first step is to register BOT channel with Azure. After login onto Azure portal, create a new resource of type “BOT Channel Registration” and provide all default settings. Follow steps in "BOT Channel Registration" for channel registration. 

For Back channel communication between SPFx webpart and BOT we will use "Direct Line". Follow the specific link on how to “Connect a Bot to Direct Line

  • App ID: Application ID
  • App secret: Application secret
  • Direct Line secret: Used by SPFX webpart to communicate with BOT

SPFx Implementation

Setup SPFx environment for development, this webpart will user current user's context to perform SharePoint search. 

Include "context" in BOT component properties

import { WebPartContext } from "@microsoft/sp-webpart-base";
 
export interface IBotProps {
  description: string;
  context:WebPartContext
}
Pass context from webpart to component

const element: React.ReactElement<IBotProps > = React.createElement(
  Bot,
  {
    description: this.properties.description,
    context:this.context
  }
);
 
ReactDom.render(element, this.domElement);

Also include following npm libraries to use Bot framework webchat and sp-pnp-js

npm install 'botframework-webchat' --save
npm install 'botframework-directlinejs' --save
npm install 'sp-pnp-js' --save

More details on botframework-webchat can be found here.

BOT component code will be as follows

import * as React from 'react';
import styles from './Bot.module.scss';
import { IBotProps } from './IBotProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { App, Chat } from 'botframework-webchat';
import { DirectLine } from 'botframework-directlinejs';
require('../../../../node_modules/botframework-webchat/botchat.css');
import { sp } from 'sp-pnp-js';
 
export default class Bot extends React.Component<IBotProps, {}> {
  public render(): React.ReactElement<IBotProps> {
    //Registering to Direct Line to communicate with BOT
    var botConnection = new DirectLine({
      secret: "DIRECT_LINE_SECRET"
    });
    //Current User information from Context
    var user = { id: this.props.context.pageContext.user.email, name: this.props.context.pageContext.user.displayName };
 
    //Sending BOT "event" type dialog with user basic information for greeting.
    botConnection
      .postActivity({ type: "event", name: "sendUserInfo", value: this.props.context.pageContext.user.displayName, from: user })
      .subscribe(id => console.log("success", id));
     
     
    //Subscribing for activities created by BOT
    var act: any = botConnection.activity$;
    act.subscribe(
      a => {
        if (a.type == "event" && a.name == "search") {
          sp.search("SPContenttype:document").then((results) => {
            var items = results.PrimarySearchResults.map((value) => {
              return {
                "Title": value.Title,
                "FileExtension": value.FileExtension,
                "Author": value.Author,
                "SubText": value.HitHighlightedSummary,
                "PictureUrl": value.PictureThumbnailURL,
                "redirectUrl": value.ParentLink
              }
            })
            botConnection
              .postActivity({ type: "message", text: "showresults", value: items, from: user })
              .subscribe(id => { console.log("success", id) });
          })
        }
      }
    );
 
    return (
      <div className={styles.bot} style={{ height: 700 }}>
        <Chat botConnection={botConnection} adaptiveCardsHostConfig={null} directLine={{ secret: "DIRECT_LINE_SECRET" }} bot={{ id: 'botid' }} user={user} />
      </div>
    );
  }
}

Above code with registering using Direct Line to communicate with BOT and pass user basic information using “postActivity” for initial Greeting message. It also subscribes for activities raised by BOT. If activity raised by BOT is of type event and of name “search”, we will perform sp-pnp-js search to get results. These results will be then passed as a simple array to BOT which will present in Hero element format.

Setup BOT using Node.js 

Follow steps to “Create a bot with the Bot Builder SDK for Node.js”. Also install ngrok.exe which will be used to expose localhost URL for Azure. 

For more information on how to expose localhost URL to Azure BOT registration, check this link

BOT Implementation 

Our conversation will use BOT's dialog and define conversation steps in waterfall structure. More on BOT dialog/message/activities/Back channel can be found here.

One important concept to understand here is "session address”, it determines where to send back the reply from BOT. In our example, we are using two separate ways of communication

  • Session direct message which will prompt the user on Chat window
  • Back channel communication with SPFx webpart using events

For a direct messages on Chat window, we keep the address information in “baseAddress” variable which will get populated in starting of a new conversation.

For Back channel, address is getting populated from the first load event raised by SPFx.

BOT Code

var restify = require('restify');
var builder = require('botbuilder');
 
var mainMenuItems = {
    "Search for Document": {
        id: "searchDocument",
        Description: "Search for Document"
    }
}
 
var searchDocItems = {
    "General Document": {
        id: "generalDoc"
    }
}
 
// Setup Restify Server
var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function () {
    console.log('%s listening to %s', server.name, server.url);
});
 
// Create chat connector for communicating with the Bot Framework Service
var connector = new builder.ChatConnector({
    appId: "APP_ID",
    appPassword: "APP_PASSWORD"
});
 
// Listen for messages from users
server.post('/api/messages', connector.listen());
 
var bot = new builder.UniversalBot(connector, [function (session) {
    baseAddress = session.message.address;
    var reply = new builder.Message(session).text('Hi  %s! Greetings !', session.message.user.name);
    session.send(reply);
    session.beginDialog("mainMenu");
},
function (session, result) {
    if (result) {
        session.beginDialog("document");
    }
}])
 
var baseAddress = null;
var address = null;
bot.dialog("mainMenu", [function (session) {
    builder.Prompts.choice(session, "What you looking for?", mainMenuItems, { listStyle: builder.ListStyle.button });
},
function (session, results) {
    if (results.response) {
        var selectedItem = mainMenuItems[results.response.entity];
        builder.Prompts.text(session, `Let me help you with  : ${selectedItem.Description}`);
        session.userData.selectedChoice = selectedItem;
        session.endDialogWithResult({ response: selectedItem });
    }
}])
 
bot.dialog("document", [function (session) {
    builder.Prompts.choice(session, "What type of document you looking for?", searchDocItems, { listStyle: builder.ListStyle.button });
},
function (session, results) {
    var ev = createEvent("search", results.response.entity, address);
    session.send(ev);
}]);
 
const createEvent = (eventName, value, address) => {
    var msg = new builder.Message().address(address);
    msg.data.type = 'event';
    msg.data.name = eventName;
    msg.data.value = value;
    return msg;
};
 
bot.dialog('showresults', function (session, args, next) {
    var msg = new builder.Message().address(baseAddress);
    msg.attachmentLayout(builder.AttachmentLayout.carousel);
    var items = session.message.value;
    var heroCards = items.map(x => {
        return new builder.HeroCard(session)
            .title(x.Title.substring(0, x.Title.indexOf('.')))
            .subtitle(x.FileExtension)
            .text(x.SubText)
            .buttons([
            ])
    })
    msg.attachments(heroCards);
    session.send(msg).endDialog();
}).triggerAction({
    matches: /^showresults$/i,
})
 
bot.on("event", function (event) {
    if (event.name == "sendUserInfo")
        address = event.address;
})

There are three dialog sections in the above code.

Entry dialog for BOT is "mainMenu" dialog which will welcome user and 'Search a Document' options. Currently, for demo purpose it has only one option, on selection of "Search a Document" - it will launch "document" dialog. "Document" dialog will fire an event to SPFx webpart which will run the search query in user's context and return results to BOT in a simple array object.

BOT is also listening for any message which matches 'showresults'. As, after the search, SPFx will return results with a message text as 'showresults' and this BOT will display those results in Hero element.

Check the video for overall functionality 

References