Angular 6 Chatbot Client

Door Yuri Burger In Applicatieontwikkeling

Robots

Azure Bot Service speeds up development by providing an integrated environment that’s purpose-built for bot development with the Microsoft Bot Framework connectors and BotBuilder SDKs.

The SDKs provide several ways for your bot to interact with people. Azure Bot Service can be integrated across multiple channels to increase interactions and reach more customers using your website or app to email, GroupMe, Facebook Messenger, Kik, Skype, Slack, Microsoft Teams, Telegram, text/SMS, Twilio, Cortana, and Skype for Business.

Another way for users to interact with your bot is through your own custom built client. In this post, we will see how to setup an Angular client and connect it to your Azure deployed Bot.

Create our bot

Let’s setup our Azure Bot first. There are several ways to do this, but in this case I will create and deploy the bot from the Microsoft Bot Framework Developer Portal https://dev.botframework.com .

Portal

If you click on “My bots” you will need to login. It is best to use an account that also has some Azure credits or MSDN benefits associated.

After login, you can start with creating our first bot 🙂 To start, click the button “Create a bot” and complete the configuration by accepting most of the defaults.

bot2

As I mentioned, bots have different ways to interact and we need to enable the desired channels. To do this, navigate to your bot either through the Bot Framework developer portal or directly with the Azure portal.

Click channels:

bot3

Your custom client can communicate with the bot through a REST API, but the preferred way is with the DirectLine client. This channel is not enabled by default, so we start with that:

bot4 From this page we need to take note of the Secret keys as this is a required setting in our custom client.

When this is done, the final piece of information we require is the messaging endpoint. The Overview page shows this url and is usually something like https://yourbotname.azurewebsites.net/api/messages

Now we can head over to our custom client.

There are no design requirements for working with the DirectLine API. Any JavaScript client should be able to work with this, but the nature of bot messaging suits Angulars “new” asynchronous programming concept very nicely. This Reactive Programming concept is based on data streams and available to Angular through the RxJS library. More information on this topic is found in the docs: https://angular.io/guide/rx-library

The sample app in this post is built using RxJS. This concept can be a little hard to grasp at first, but the main parts will be explained here. I will not cover RxJS itself, so you might want to read up on that before running your own client.

The app is built upon three services: users, threads and messages. All these services rely on a specific model (a user, thread and message). During app setup, we hook up the message service to our bot with the help of the Direct Line client.

User Model and Service

The User Service is pretty basic. It exposes a currentUser stream which any part of our application can subscribe to and know who the current user is.

export class UsersService {
  currentUser: Subject<User> = new BehaviorSubject<User>(null);

  public setCurrentUser(newUser: User): void {
    this.currentUser.next(newUser);
  }
}

The user model that goes with this, is also pretty basic:

export class User {
  id: string;

  constructor(public name: string, public avatarSrc: string) {
    this.id = uuid();
  }
}

Thread Model and Service

Threads play an important role in our app as they are responsible for “threading” the message streams. In this case we only have one bot and thus one thread, but if we extend the scenario we could support multiple bots/ threads.

export class ThreadsService {
  threads: Observable<{ [key: string]: Thread }>;
  currentThread: Subject<Thread> = new BehaviorSubject<Thread>(new Thread());
  currentThreadMessages: Observable<Message[]>;

  constructor(public messagesService: MessagesService) {
    this.threads = messagesService.messages.pipe(
      map((messages: Message[]) => {
        const threads: { [key: string]: Thread } = {};
        // Store the message's thread in our accumulator threads
        messages.map((message: Message) => {
            // code omitted
            // see sample app on GitHub
        return threads;
      })
    );

    this.currentThreadMessages = combineLatest(
      this.currentThread,
      messagesService.messages,
      (currentThread: Thread, messages: Message[]) => {
        // code omitted
        // see sample app on GitHub
      }
    );
  }

  setCurrentThread(newThread: Thread): void {
    this.currentThread.next(newThread);
  }
}

This is RxJS in full action and you can see we expose some important streams:

currentThread: Subject stream (read/write) that holds the select stream.
currentThreadMessages: an Observable stream that contains our future messages.

The model is straight forward:

export class Thread {
  id: string;
  lastMessage: Message;
  name: string;
  avatarSrc: string;

  constructor(id?: string, name?: string, avatarSrc?: string) {
    this.id = id || uuid();
    this.name = name;
    this.avatarSrc = avatarSrc;
  }
}

Message Model and Service

This is the heart of the app as it handles all of our message concerns.

export class MessagesService {
  newMessages: Subject<Message> = new Subject<Message>();
  messages: Observable<Message[]>;
  updates: Subject<any> = new Subject<any>();
  create: Subject<Message> = new Subject<Message>();

  constructor() {
    this.messages = this.updates
      // Watch the updates and accumulate operations on the messages
      .pipe(
        // code omitted
        // see sample app on GitHub
      );

    // Takes a Message and then puts an operation (the inner function)
    // on the updates stream to add the Message to the list of messages.
    this.create
      .pipe(
        map(function(message: Message): IMessagesOperation {
          return (messages: Message[]) => {
            return messages.concat(message);
          };
        })
      )
      .subscribe(this.updates);

    this.newMessages.subscribe(this.create);
  }

  // Add message to stream
  addMessage(message: Message): void {
    this.newMessages.next(message);
  }

  messagesForThreadUser(thread: Thread, user: User): Observable<Message> {
    return this.newMessages.pipe(
      filter((message: Message) => {
        // code omitted
        // see sample app on GitHub
      })
    );
  }
}

There is a lot going on here, most of it related to RxJS. We now have code that adds new messages to the stream and code that exposes a stream with messages for a certain thread (messagesForThreadUser). This last stream is what actually feeds the Angular Chat Window component.

The model for our messages:

export class Message {
  id: string;
  sentAt: Date;
  isRead: boolean;
  author: User;
  text: string;
  thread: Thread;

  constructor(obj?: any) {
    this.id = (obj && obj.id) || uuid();
    this.isRead = (obj && obj.isRead) || false;
    this.sentAt = (obj && obj.sentAt) || new Date();
    this.author = (obj && obj.author) || null;
    this.text = (obj && obj.text) || null;
    this.thread = (obj && obj.thread) || null;
  }
}

To add the DirectLine client to our app, we can make use of the Direct Line client npm package. To install it:

npm install botframework-directlinejs --save

Please note: Although the demo app itself is written for RxJS 6, the botframework-directlinejs still requires rxjs-compat to be available.

With a setup class we configure the app:

export class Setup {
  static init(
    messagesService: MessagesService,
    threadsService: ThreadsService,
    usersService: UsersService
  ): void {
    messagesService.messages.subscribe(() => ({}));
    usersService.setCurrentUser(me);
    threadsService.setCurrentThread(tMoneyPenny);

    this.setupBots(messagesService);
  }

  static setupBots(messagesService: MessagesService): void {
    // Send our messages to Miss Moneypenny
    messagesService
      .messagesForThreadUser(tMoneyPenny, moneypenny)
      .forEach((message: Message): void => {
        directLine
            // code omitted
            // see sample app on GitHub
      }, null);

    // Watch incoming messages from our bot
    directLine.activity$
        // code omitted
        // see sample app on GitHub
  }
}
  1. It subscribes to the messages
  2. Sets the current user and current thread
  3. Connects the message service stream to the bot through the Direct Line client for outgoing and incoming messages

Pfffew finally we can work on our Angular components. Fortunately this is now pretty straight forward: In our app.component On Init we subscribe to the currentThread and set our currentUser.

ngOnInit(): void {
    this.messages = this.threadsService.currentThreadMessages;
    this.draftMessage = new Message();
    this.threadsService.currentThread.subscribe((thread: Thread) => {
      this.currentThread = thread;
    });

    this.usersService.currentUser.subscribe((user: User) => {
      this.currentUser = user;
    });
  }

And to complete the app, we have a chat-message component to render the HTML:

<div>
    <span class="chat-time">
        {{message.author.name}} | {{message.sentAt | fromNow}}
    </span>
    {{message.text}}
</div>
<div class="msg-sent">
    <span class="chat-time">
        {{message.sentAt | fromNow}} | {{message.author.name}}
    </span>
    <p class="msg-sent">{{message.text}}</p>
</div>

Please check out the complete sample app on GitHub: https://github.com/yuriburger/ng-chatbot-demo

If all is setup correctly, you should have a working Chatbot client:

bot5

Happy chatting!
/Y.

Meer informatie

Yuri Burger - VX Company

Yuri Burger

Principal Consultant

+31 6 11 75 16 83 Stuur Yuri een e-mail

Reacties

Er zijn nog geen reacties op dit bericht.

Plaats een reactie

Dit veld is verplicht.

Vul een geldig e-mailadres in.

Dit veld is verplicht.