jeudi 27 juillet 2017

Bot Framework Part 5 - Déployer un Chatbot

Dans cet article, nous allons aborder toutes les étapes nécessaires à la mise en production d'un Chatbot

  • Enregistrer notre bot sur le portail Bot Framework
  • Déployer notre Chatbot sur une instance de site web dans Azure
  • Tester notre Chatbot
  • Emuler différents channels : Slack, Teams, Skype, Facebook Messenger etc.
Notre Chatbot est simplement une application web et de ce fait il est possible de l'héberger sur tout host permettant d'héberger une site web ASP.net (Azure, AWS etc.)
Pour enregistrer notre Bot, il suffit de se connecter au portail dev.botframework.com en s'authentifiant, puis cliquer sur l'onglet My Bots
Puis cliquer sur Register pour enregistrer notre Chatbot, nous complétons alors le nom du Chatbot, le Handle qui est à usage interne uniquement et ne doit pas contenir d'espaces et enfin une description du Chatbot
Pour la configuration, elle sera fournie par le portail, mais il faut tout de même en entrer une, nous pouvons mettre temporairement : https://blank-url.com puis cliquer sur  le  bouton Create Microsoft App ID
Nous sommes alors rediriger vers le portail de développement Microsoft avec la génération de l'ID
Cliquer sur le bouton générer Mot de passe, la fenêtre suivante s'affiche alors
Attention à bien copier l'App ID et le Password dans un endroit ou vous saurez le retrouver, nous en aurons besoin pour modifier la configuration du Chatbot ultérieurement. Il ne sera affiché qu'une seule fois tel qu'indiqué dans la fenêtre popup
Enfin, on renseigne l'adresse email Admin (notre compte LiveID) puis simplement cliquer le bouton Register
Nous devons maintenant configurer et déployer notre Chatbot, pour cela revenons dans Visual Studio, ouvrir le fichier Web config et complèter les variables App ID et Password avec les valeurs générées lors de l'enregistrement du Chatbot

Puis nous allons déployer le Chatbot sur une instance Web Azure, pour cela simplement lancer la commande Publish à partir du projet Chatbot et choisir la création d'une nouvelle Microsoft Azure App Service
Visual Studio, nous propose la fenêtre de création en renseignant à partir de nos données de profil, simplement cliquer sur le bouton Create
Une fois généré, nous récupérons l'URL du Chatbot afin de la remplacer dans la page de définition de notre bot et donc remplacer l'URL https://blank-url.com par celle générée.
Nous revenons dans le portail bot Framework et remplacons l'adresse sans oublier de la suffixer par : /api/messages et préfixer par https, puis cliquer sur Save Changes en bas de page




Dernière étape, tester le Chatbot, pour cela, simplement cliquer sur le bouton Test, une fenêtre de dialogue s'ouvre, le Chatbot devrait répondre tel qu'attendu : 
Pour terminer, nous savons qu'il n'est absolument pas une bonne idée de laisser des mots de passe ou identifiants dans le controle de code source, pour cela nous allons retirer l'App ID et le password du web.config pour le reporter dans la section App Settings de notre App service, en créeant simplement les 2 Clé valeurs comme montré ci dessous

Ne pas oublier de sauvegarder
Nous allons par exemple pouvoir l'intégrer dans Skype par exemple, à partir de la page de votre Bot, cliquer sur Skype
Puis cliquer sur Add to Contacts et lancer Skype
Ci dessous, la conversation avec le Bot intégrée dans Skype

Pour tous les autres logiciels conversationnels, il suffit de cliquer sur celui que l'on veut intégrer et simplement suivre la procédure


mercredi 26 juillet 2017

Bot Framework Part 4 - Optimisez l'Interaction avec l'utilisateur avec les Prompts

Un des objectifs clés d'un Chat Bot est la contribution de valeur  vis à vis de l'utilisateur, et ceci va passer notamment par des interfaces de dialogue performantes dont l'effet sera le gain de temps. Le premier vecteur va bien évidemment consister à limiter les échanges par texte obligeant l'utilisateur à utiliser son clavier tactile sur un téléphone par exemple ce qui est clairement peu productif et fastidieux pour l'utilisateur. Pour pallier à cela on trouvera des solutions très interessantes dans le Bot Framework tel que les Prompts qui vont permettre d'accèlérer la collecte d'informations sous la forme d'options à cliquer sous différentes forme, l'idée étant de minimiser au maximum la frappe au clavier au profit de boutons ou d'options à cliquer, l'idée est également de cadrer l'utilisateur afin qu'il ne se perde pas dans des conversations infructueuses avec le Chat Bot, on aura également le fait d'apporter de l'aide à l'utilisaeur pour connaitre les possibilités du Chat Bot et donc optimiser le service rendu à l'utilisateur.
Parmi les différents types de Prompts on aura : 
  • Des formulaires pour la collecte de texte
  • La détection de type de données : Nombres, Date - Heure
  • Des listes d'options pour des choix guidés
  • Des Media : images, vidéos par exemple pour des propositions d'hotels ou restaurants
Un des scenarios les plus courant est l'implémentation de la recherche, avec de manière générique les  étapes suivantes : 
  1. Détecter que l'utilisateur est à la recherche de quelque chose : en lui posant la question :)
  2. Recueillir de la part de l'utilisateur une requéte en text Prompt
  3. Proposer des options de réponses cliquables
  4. Afficher le résultat, potentiellement un carousel d'images avec du texte
Nous allons donc mettre en oeuvre ce scénario avec un exemple dans lequel nous recherchons des Profils dans GitHub et affichons la liste de résultats sous la forme d'un carrousel d'options cliquables pour accèder au profil 

Le code du Message Controller qui collecte les messages et répond à l'utilisateur est toujours le même :


using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;

namespace Bot_Application
{
    [BotAuthentication]
    public class MessagesController : ApiController
    {
        ///




        /// POST: api/Messages
        /// Receive a message from a user and reply to it
        ///

        public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity.Type == ActivityTypes.Message)
            {
                await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
            }

            var response = Request.CreateResponse(HttpStatusCode.OK);
            return response;
        }
    }
}


Le code de RootDialog.cs ci dessous gère le déclenchement de la recherche dès que l'utilsateur aura entré le mot recherche au début de la conversation, si l'utilisateur tape une chaine commencant par le mot recherche suivi d'un espace ou de texte, on va gèrer le cas également comme montré ci dessous. Une fois la requète entrée, la recherche est lancée: 

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Bot_Application.Dialogs
{
    [Serializable]
    public class RootDialog : IDialog<object>
    {
        public Task StartAsync(IDialogContext context)
        {
            context.Wait(MessageReceivedAsync);

            return Task.CompletedTask;
        }

        private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
        {
            var msg = await result as IMessageActivity;
            if (msg.Text.Equals(@"Recherche", StringComparison.OrdinalIgnoreCase))
            {
                PromptDialog.Text(context, QueryEntered, @"Que recherchez vous?");
            }
            else if (msg.Text.StartsWith(@"Recherche ", StringComparison.OrdinalIgnoreCase))
            {
                var query = msg.Text.Substring(7);
                await context.Forward<string, string>(new SearchDialog(), SearchComplete, query, default(CancellationToken));
            }
        }

        private async Task QueryEntered(IDialogContext context, IAwaitable<string> result)
        {
            await context.Forward<string, string>(new SearchDialog(), SearchComplete, await result, default(CancellationToken));
        }

        private async Task SearchComplete(IDialogContext context, IAwaitable<string> result)
        {
            var returnMessage = await result;
            if (!string.IsNullOrWhiteSpace(returnMessage)) await context.PostAsync(returnMessage);

            context.Wait(MessageReceivedAsync);
        }

    }
}

Nous avons également un client de recherche sommaire pour GitHub qui va nous retourner les informations de base d'un profil recherché sur GitHub, GitHubClient.cs: 


using Octokit;
using System;
using System.Threading.Tasks;
using gh = Octokit;

namespace Bot_Application
{
    public static class GitHubClient
    {
        private static readonly LazyGitHubClient> _client = new LazyGitHubClient>(() => new gh.GitHubClient(new ProductHeaderValue(@"MyBotApp")));

        public static TaskUser> LoadProfile(string username) => _client.Value.User.Get(username);

        public static TaskSearchUsersResult> ExecuteSearch(string query) => _client.Value.Search.SearchUsers(new SearchUsersRequest(query));
    }

}

Enfin, la classe SearchDialog.cs a pour responsabilité d'exécuter la recherche et de retourner une liste de cards sour la forme d'un carrousel de profils GitHub. On remarquera la facilité avec laquelle on crée la carte de profile avec la méthode CreateCard : 

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace Bot_Application.Dialogs
{
    [Serializable]
    internal class SearchDialog : IDialog<string>
    {
        public Task StartAsync(IDialogContext context)
        {
            context.Wait<string>(MessageReceivedAsync);

            return Task.CompletedTask;
        }

        private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<string> result)
        {
            var query = await result;

            var profiles = await GitHubClient.ExecuteSearch(query);

            var totalCount = profiles.TotalCount;

            if (totalCount == 0)
            {
                context.Done(@"Désolé, Pas de résultat trouvé.");
            }
            else if (totalCount > 10)
            {
                context.Done(@"Plus de 10 résultats trouvés. Pouvez vous filtrer plus précisément?");
            }
            else
            {
                // convert the results in to an array of cards for each user
                var userCards = profiles.Items.Select(item => CreateCard(item));

                var msg = context.MakeMessage();
                msg.AttachmentLayout = AttachmentLayoutTypes.Carousel;
                msg.Attachments = userCards.ToList();

                await context.PostAsync(msg);
                context.Done(default(string));
            }
        }

        private static Attachment CreateCard(Octokit.User profile) =>
            new ThumbnailCard()
            {
                Title = profile.Login,
                Images = new[] { new CardImage(url: profile.AvatarUrl) },
                Buttons = new[] { new CardAction(ActionTypes.OpenUrl, @"Cliquer pour consulter", value: profile.HtmlUrl) }
            }.ToAttachment();
    }
}

Voyons le résultat obtenu, on lance le Service Bot
Puis on lance l'émulateur Bot et on se connecte à l'URL : http://localhost:3979/api/messages
puis on entre le mot recherche pour déclencher la conversation avec le chat bot


On voit ici le résultat affiché sous la forme d'un carrousel de cartes, et si l'on clique sur consulter, on a accès à la fiche GitHub : 

Cet article nous a permis de montré que nous pouvons au travers d'un Chat Bot implémenter des scénarios ergonomiquement interessant et plus interactif en tirant parti des différents Prompts proposés par le Bot Framework.
Dans l'article suivant nous verrons comment déployer notre Bot afin de pouvoir le distribuer pour un usage en ligne

Cette gestion d'état et de persistence va permettre des interactions beaucoup plus riche avec le Chat Bot.
Vous retrouverez le code de cet exemple à l'adresse : Code Chatbot

lundi 24 juillet 2017

Bot Framework Part 3 - Utilisation du Bot Workflow pour gérer la persistence du contexte d'un dialogue

lors des précédents articles, nous avons vu la mise en oeuvre simple d'un Chat Bot avec des fonctionnalités extrêmement limitées, par exemple notre Bot ne gérait aucune persistence de la conversation, donc incapacité à maintenir une conversation sur un sujet ou bien encore de se souvenir de vous dès le début d'une nouvelle conversation. Nous allons voir dans cet article que tout ceci est possible au travers de différents niveaux de persistence des données avec dans l'idée, la capactité à maintenir l'état d'une conversation. Par exemple si vous démarrez une conversation avec un Chat Bot en lui demandant quel est le monument le plus haut de Paris, et que le Bot vous répond La Tour Eiffel, si vous lui demandez qui l'a construite, sans persistence, il ne saura répondre car il aura perdu l'information précédente, donc avec ce système, nous allons pouvoir développer des expériences bien plus proches du réel.
Donc le besoin de maintenir des états est quelque chose de très important, cette fonctionnalité sera fournie par le Bot Connector dont le contrat est de router les messages et gérer la persistence et l'état d'un dialogue. Le Bot fonctionne en tant que Web Service et a donc une communication sans état (stateless) et c'est donc bien la responsabilité du Bot Connector d'assurer cette persistence d'état, notamment par Bot et par utilisateur. Le stockage de l'état se fait en base de données, et on aura un UserID, un ConversationID etc.
Dans l'exemple ci dessous, nous allons donner à notre robot notre prénom et notre ville, grace à la gestion de persistence, il sera capable de restituer toutes ces informations stockées sur notre profil
La première chose à faire est de créer une classe sérialisable décrivant nos informations de profil : UserProfile : 
namespace Bot_Application.Dialogs
{
    [Serializable]
    public class UserProfile
    {
       public string Name { get; set; }
       public string Country { get; set; }
    }
}

Puis nous allons créer la classe EnsureProfileDialog.cs dont la responsabilité va être de s'assurer de la capture des informations de profil de l'utilisateur, dans notre cas, le prénom et la ville

using Microsoft.Bot.Builder.Dialogs;
using System;
using System.Threading.Tasks;

namespace Bot_Application.Dialogs
{
    [Serializable]
    public class EnsureProfileDialog : IDialog<UserProfile>
    {
        public Task StartAsync(IDialogContext context)
        {
            EnsureProfileName(context);

            return Task.CompletedTask;
        }

        UserProfile _profile;
        private void EnsureProfileName(IDialogContext context)
        {
            if (!context.UserData.TryGetValue(@"profile", out _profile))
            {
                _profile = new UserProfile();
            }

            if (string.IsNullOrWhiteSpace(_profile.Name))
            {
                PromptDialog.Text(context, NameEntered, @"Bonjour, quel est votre nom?");
            }
            else
            {
                EnsureCountryName(context);
            }
        }

        private void EnsureCountryName(IDialogContext context)
        {
            if (string.IsNullOrWhiteSpace(_profile.Country))
            {
                PromptDialog.Text(context, CountryEntered, @"Ou habitez vous?");
            }
            else
            {
                context.Done(_profile);
            }
        }

        private async Task CountryEntered(IDialogContext context, IAwaitable<string> result)
        {
            _profile.Country = await result;

            context.Done(_profile);
        }

        private async Task NameEntered(IDialogContext context, IAwaitable<string> result)
        {
            _profile.Name = await result;

            EnsureCountryName(context);
        }
    }
}

La classe RootDialog va permettre le stockage des données de profil dans le User Data Bag, cette tache est effectuée par la tache ProfileEnsured

using System;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;

namespace Bot_Application.Dialogs
{
    [Serializable]
    public class RootDialog : IDialog<object>
    {
        public Task StartAsync(IDialogContext context)
        {
            context.Wait(MessageReceivedAsync);

            return Task.CompletedTask;
        }

        private Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
        {
            context.Call<UserProfile>(new EnsureProfileDialog(), ProfileEnsured);

            return Task.CompletedTask;
        }

        private async Task ProfileEnsured(IDialogContext context, IAwaitable<UserProfile> result)
        {
            var profile = await result;

            context.UserData.SetValue(@"profile", profile);

            await context.PostAsync($@"Bonjour {profile.Name}, j'adore {profile.Country}!");

            context.Wait(MessageReceivedAsync);
        }
    }

 
}

Enfin la classe  MessageController.cs

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;

namespace Bot_Application
{
    [BotAuthentication]
    public class MessagesController : ApiController
    {
        ///


        /// POST: api/Messages
        /// Receive a message from a user and reply to it
        ///

        public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity.Type == ActivityTypes.Message)
            {
                await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
            }

            var response = Request.CreateResponse(HttpStatusCode.OK);
            return response;
        }
    }
}

Regardons maintenant le résultat obtenu en lançant le Bot avec F5 : 
On lance alors l'émulateur et on se connecte sur le port 3979/api/messages, on voit bien les 2 questions posées par le robot, qui termine par la restitution de l'ensemble de l'information en requêtant le User Data Bag

Cette gestion d'état et de persistence va permettre des interactions beaucoup plus riche avec le Chat Bot.
Vous retrouverez le code de cet exemple à l'adresse : Code Chatbot