[Actualité] Bot Builder V4 : mise en place d’une conversation guidée avec la librairie Dialogs
par
, 03/11/2018 à 18h01 (4752 Affichages)
Le Bot Builder SDK V4 pour .NET permet de mettre en place des agents conversationnels en utilisant le langage de programmation C#. Ces types d’application sont désormais utilisés par de nombreuses entreprises pour offrir de nouvelles expériences à travers des assistants virtuels. D’où l’engouement actuel constaté pour ce type de solution.
Dans mon premier billet introductif sur le Bot Builder SDK V4, je fournis le minimum nécessaire pour mettre en place un bot. Dans ce nouveau billet, nous verrons comment utiliser une librairie centrale du SDK pour créer avec souplesse et simplicité une conversation guidée.
Il est quasi-impossible de mettre en place un système de conversation performant dans un bot sans utiliser la librairie Dialogs. La gestion des conversations offerte par le SDK est très centrée sur les dialogues.
Les objets de type Dialog traitent les activités entrantes et génèrent les réponses sortantes. La logique métier du bot s’exécute directement ou indirectement dans les classes de dialogue.
Lors de l’exécution du bot, les instances de dialogue sont organisées dans une pile. Le dialogue actif est toujours en haut de la pile. Le dialogue actif traite l’activité entrante.
Trois fonctions essentielles sont offertes par chaque objet de type Dialog :
- BeginDialogAsync
- ContinueDialog
- EndDialogAsync
Nous y reviendrons plus tard, dans la mise en place de notre application.
Les « Prompt »
La librairie Dialogs offre différents Prompt qui peuvent être utilisés pour collecter différents types d’entrées utilisateur. Pour demander, par exemple, à un utilisateur de saisir un numéro, vous pouvez utiliser NumberPrompt. Pour des saisir de texte, vous pouvez utiliser TextPrompt. Ci-dessous les Prompt offerts par le SDK :
- AttachmentPrompt : utilisé pour demander à l’utilisateur de fournir des fichiers (images, pdf, etc.) ;
- ChoicePrompt : utilisé pour proposer une liste de choix parmi lesquels l’utilisateur doit faire une sélection ;
- ConfirmPrompt : utilisé pour demander la confirmation de l’utilisateur (Oui/Non) ;
- DateTimePrompt : utilisé pour demander la saisie d’une datetime ;
- NumberPromt : utilisé pour demander la saisie d’un nombre ;
- OAuthPrompt : utilisé pour implémenter l’authentification ;
- TextPrompt : utilisé pour demander la saisie de texte.
Chaque Prompt implémente un dialogue en deux étapes. En premier, Le Prompt demande à l’utilisateur de fournir une information. Ensuite, il valide la valeur et la retourne au programme ou ré-affiche le message précédent si la saisie de l’utilisateur n’est pas valide.
Waterfall Dialog
Dans notre exemple, nous allons utiliser des dialogues en cascade (Waterfall dialog). Il s’agit d’une séquence d’étapes qui seront exécutées en cascade. Chaque étape est en principe une fonction. La première fonction qui sera exécutée affichera un Prompt pour demander une entrée à l’utilisateur. Les fonctions qui suivront traiteront les résultats du Prompt précédent et feront appel au Prompt suivant ou mettront fin à la séquence.
Le diagramme ci-dessous montre la séquence d’exécution d’un dialogue en cascade.
Dialogs State
Les états sont utilisés pour le suivi des conversations et la persistance des informations. Les données d’état sont utilisées par le Bot pour se souvenir par exemple des réponses aux questions précédentes ou pour prendre des décisions.
L’état du dialogue est un élément essentiel à maîtriser lorsqu’on utilise la librairie Dialogs. En effet, les dialogues reposent sur un état persistant. Lors de l’initialisation d’un sous-système de dialogue, le bot a besoin de l’état pour créer le contexte du dialogue.
La lecture et la mise à jour des états se font au travers des accesseurs. Chaque application doit implémenter un BotAccessors. L’accesseur doit disposer des propriétés pour chaque état manipulé par le bot.
Mise en place du bot
Maintenant que nous sommes familiers avec certains concepts clés concernant la librairie Dialogs, passons à la mise en place de notre solution.
Notre bot est un assistant qui permettra de récupérer des feedbacks des utilisateurs. Ce dernier doit dans un premier temps demander le consentement de l’utilisateur. Si l’utilisateur ne donne pas son consentement, la conversation prend fin. Si ce dernier donne son consentement, le bot demande ensuite son Nom, son adresse mail puis le message de ce dernier.
Ci-dessous le diagramme de séquence correspondant :
Pour commencer, nous allons créer une nouvelle application en utilisant le modèle « Bot Builder Echo Bot V4 ».
Ensuite, vous devez installer via le gestionnaire de packages NuGet le package Microsoft.Bot.Builder.Dialogs :
Mise en place des états
Notre bot via son accesseur va manipuler les états/informations suivantes :
- ConversationState : permet au bot d’effectuer le suivi de la conversation en cours entre lui-même et l’utilisateur.
- UserState : peut servir à de nombreuses fins, par exemple pour déterminer l’endroit où une conversation précédente de l’utilisateur s’était arrêtée ou simplement pour accueillir un utilisateur régulier par son nom. Si vous stockez les préférences d’un utilisateur, vous pouvez utiliser ces informations pour personnaliser la prochaine conversation.
- DialogState : permet au bot d’effectuer un suivi dans la pile de dialogues
- FeedBackData : permet au bot de se souvenir des réponses que l’utilisateur va donner à chaque étape.
Des classes sont fournies par le Framework pour les trois premiers états. Nous devons créer une classe FeedBackData pour le dernier cas. Vous devez donc ajouter le fichier correspondant à votre projet avec les lignes de code suivantes :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 public class FeedbackData { public string Name { get; set; } public string Email { get; set; } public string Message { get; set; } }
Vous devez ensuite modifier le fichier EchoBotAccessors.cs et remplacer la classe par celle-ci :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 public class BotAccessors { /// <summary> /// Initializes a new instance of the <see cref="BotAccessors"/> class. /// Contains the <see cref="ConversationState"/> and associated <see cref="IStatePropertyAccessor{T}"/>. /// </summary> /// <param name="conversationState">The state object that stores the conversationState.</param> /// <param name="userState">The state object that stores the userState.</param> public BotAccessors(ConversationState conversationState, UserState userState) { ConversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState)); UserState = userState ?? throw new ArgumentNullException(nameof(userState)); } /// <summary> /// Gets or sets the <see cref="IStatePropertyAccessor{T}"/> for FeedbackData. /// </summary> /// <value> /// The accessor stores the FeedbackData for the conversation. /// </value> public IStatePropertyAccessor<FeedbackData> FeedbackData { get; set; } /// <summary> /// Gets or sets the <see cref="IStatePropertyAccessor{T}"/> for ConversationDialogState. /// </summary> /// <value> /// The accessor stores the ConversationDialogState for the conversation. /// </value> public IStatePropertyAccessor<DialogState> ConversationDialogState { get; set; } /// <summary> /// Gets the <see cref="ConversationState"/> object for the conversation. /// </summary> /// <value>The <see cref="ConversationState"/> object.</value> public ConversationState ConversationState { get; } /// <summary> /// Gets the <see cref="UserState"/> object for the conversation. /// </summary> /// <value>The <see cref="UserState"/> object.</value> public UserState UserState { get; } }
Vous avez probablement remarqué que les propriétés FeedbackData et ConversationDialogState sont de type IStatePropertyAccessor<T>. Cette interface définit des méthodes asynchrones qui permettront à l’accesseur de lire, modifier ou supprimer une propriété.
Mise en place du dialogue
Vous allez créer une nouvelle classe RequestFeedbackDialog. Elle doit hériter de l’interface IBot :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 public class RequestFeedbackDialog : IBot { public Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); } }
Ajoutez à votre classe les propriétés suivantes :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 private readonly BotAccessors _accessors; private readonly ILogger _logger; private DialogSet _dialogs;
Nous allons ensuite ajouter un constructeur à notre classe. Ce constructeur doit prendre en paramètre deux champs de type BotAccessors et ILoggerFactory.
Dans le constructeur, nous allons initialiser la variable qui va contenir tous nos dialogues :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 // The DialogSet needs a DialogState accessor, it will call it when it has a turn context. _dialogs = new DialogSet(accessors.ConversationDialogState);
Nous allons ensuite définir les étapes de notre dialogue en cascade dans un tableau de type WaterfallStep :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 var waterfallSteps = new WaterfallStep[] { RequestStepAsync, NameStepAsync, NameConfirmStepAsync, EmailConfirmStepAsync, SummaryStepAsync, };
Et pour finir, nous allons ajouter les dialogues à notre pile de dialogues :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 // Add named dialogs to the DialogSet. These names are saved in the dialog state. _dialogs.Add(new WaterfallDialog("details", waterfallSteps)); _dialogs.Add(new ConfirmPrompt("confirm", defaultLocale: Culture.French)); _dialogs.Add(new TextPrompt("nom")); _dialogs.Add(new TextPrompt("email", EmailPromptValidatorAsync)); _dialogs.Add(new TextPrompt("message"));
Ci-dessous le code complet du constructeur :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 _accessors = accessors ?? throw new ArgumentNullException(nameof(accessors)); if (loggerFactory == null) { throw new System.ArgumentNullException(nameof(loggerFactory)); } _logger = loggerFactory.CreateLogger<RequestFeedbackDialog>(); _logger.LogTrace("RequestFeedbackDialogBot turn start."); // The DialogSet needs a DialogState accessor, it will call it when it has a turn context. _dialogs = new DialogSet(accessors.ConversationDialogState); // This array defines how the Waterfall will execute. var waterfallSteps = new WaterfallStep[] { RequestStepAsync, NameStepAsync, NameConfirmStepAsync, EmailConfirmStepAsync, SummaryStepAsync, }; // Add named dialogs to the DialogSet. These names are saved in the dialog state. _dialogs.Add(new WaterfallDialog("details", waterfallSteps)); _dialogs.Add(new ConfirmPrompt("confirm", defaultLocale: Culture.French)); _dialogs.Add(new TextPrompt("nom")); _dialogs.Add(new TextPrompt("email", EmailPromptValidatorAsync)); _dialogs.Add(new TextPrompt("message"));
L’éditeur de Visual Studio va vous afficher des erreurs, car vous n’avez pas encore implémenter les fonctions pour chaque étape. Vous pouvez utiliser ses suggestions de corrections pour générer les fonctions qui doivent être implémentées :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 private Task<DialogTurnResult> RequestStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { throw new NotImplementedException(); } private Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { throw new NotImplementedException(); } private Task<DialogTurnResult> NameConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { throw new NotImplementedException(); } private Task<DialogTurnResult> EmailConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { throw new NotImplementedException(); } private Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { throw new NotImplementedException(); } private Task<bool> EmailPromptValidatorAsync(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken) { throw new NotImplementedException(); }
Etape 1 : demander l’autorisation de l’utilisateur.
L’étape 1 va être implémentée dans la fonction RequestStepAsync. Cette fonction permettra juste d’appeler le dialogue avec pour id « confirm ». Il affichera un message à l’utilisateur pour lequel il pourra répondre par oui ou non. Ci-dessous son code :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11 /// <summary> /// One of the functions that make up the <see cref="WaterfallDialog"/>. /// </summary> /// <param name="stepContext">The <see cref="WaterfallStepContext"/> gives access to the executing dialog runtime.</param> /// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param> /// <returns>A <see cref="DialogTurnResult"/> to communicate some flow control back to the containing WaterfallDialog.</returns> private async Task<DialogTurnResult> RequestStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. return await stepContext.PromptAsync("confirm", new PromptOptions { Prompt = MessageFactory.Text("Voulez-vous nous laisser vos commentaires ?") }, cancellationToken); }
Etape 2 : Traiter la réponse de l’étape 1 et poser la question suivante ou mettre fin à la conversation.
Si à l’étape 1, l’utilisateur a répondu Non, nous allons mettre fin à la conversation. Sinon, nous allons appeler le dialogue avec l’id « nom » pour lui demander de renseigner son nom. Nous allons effectuer cela dans la fonction NameStepAsync. Son code complet est le suivant :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 private async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { if ((bool)stepContext.Result) { // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. // Running a prompt here means the next WaterfallStep will be run when the users response is received. return await stepContext.PromptAsync("nom", new PromptOptions { Prompt = MessageFactory.Text("Veuillez saisir votre nom.") }, cancellationToken); } else { await stepContext.Context.SendActivityAsync(MessageFactory.Text("Merci. Au revoir."), cancellationToken); return await stepContext.EndDialogAsync(cancellationToken: cancellationToken); } }
Etape 3 : traiter la réponse de l’étape 2 et poser la question suivante
L’étape 3 est implémentée dans la fonction NameConfirmStepAsync. Nous allons à partir de l’accesseur récupérer l’objet pour stocker les informations de l’utilisateur et ensuite mettre à jour le champ Name.
Nous allons ensuite utiliser le dialogue ayant pour id « email » pour demander à l’utilisateur de renseigner son adresse mail. Le code complet de cette fonction est le suivant :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11 private async Task<DialogTurnResult> NameConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { // Get the current feedbackData object from user state. var feedbackData = await _accessors.FeedbackData.GetAsync(stepContext.Context, () => new FeedbackData(), cancellationToken); // Update the feedbackData. feedbackData.Name = (string)stepContext.Result; // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. return await stepContext.PromptAsync("email", new PromptOptions { Prompt = MessageFactory.Text("Veuillez saisir votre adresse email.") }, cancellationToken); }
Etape 4 : traiter la réponse de l’étape 3 et poser la question suivante
Cette étape est identique à l’étape précédente et est implémentée dans la fonction EmailConfirmStepAsync() :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11 private async Task<DialogTurnResult> EmailConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { // Get the current feedbackData object from user state. var feedbackData = await _accessors.FeedbackData.GetAsync(stepContext.Context, () => new FeedbackData(), cancellationToken); // Update the feedbackData. feedbackData.Email = (string)stepContext.Result; // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. return await stepContext.PromptAsync("message", new PromptOptions { Prompt = MessageFactory.Text("Veuillez saisir votre message.") }, cancellationToken); }
Etape 5 : traiter la réponse de l’étape 4, afficher le message récapitulatif et mettre fin à la conversation
A cette étape on obtient la dernière information qui a été saisie par l’utilisateur et on met fin à la conversation. Cette étape est implémentée dans la fonction SummaryStepAsync. Son code complet est le suivant :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14 private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { // Get the current feedbackData object from user state. var feedbackData = await _accessors.FeedbackData.GetAsync(stepContext.Context, () => new FeedbackData(), cancellationToken); // Update the feedbackData. feedbackData.Message = (string)stepContext.Result; await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Votre nom {feedbackData.Name}, votre email {feedbackData.Email} et votre message {feedbackData.Message}."), cancellationToken); // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is the end. return await stepContext.EndDialogAsync(cancellationToken: cancellationToken); }
Validation de l’émail
Avec le ConfirmPrompt, l’utilisateur à défaut de cliquer sur le bouton Oui ou Non dans les choix qui lui sont proposés, il doit saisir oui ou non. S’il saisit autre chose, il y a une validation qui se fera et il ne pourra pas passer à l’étape suivante.
Dans notre cas, nous voulons valider que l’utilisateur à entrer une adresse mail valide avant de passer à l’étape suivante. C’est pourquoi en ajoutant le TextPromt pour l’émail dans les dialogues, nous avons renseigné le nom d’une fonction de validation :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part _dialogs.Add(new TextPrompt("email", EmailPromptValidatorAsync));
Cette fonction doit retourner un task<bool> et prendre en paramètre le contexte du Prompt et le CancellationToken. Le contexte du Prompt va permettre de récupérer l’entrée qui a été saisie par l’utilisateur et y appliquer notre validation :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 private Task<bool> EmailPromptValidatorAsync(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken) { var result = promptContext.Recognized.Value; // This condition is our validation rule. if (IsValidEmail(result)) { // Success is indicated by passing back the value the Prompt has collected. You must pass back a value even if you haven't changed it. return Task.FromResult(true); } // Not calling End indicates validation failure. This will trigger a RetryPrompt if one has been defined. return Task.FromResult(false); } private bool IsValidEmail(string source) { return new EmailAddressAttribute().IsValid(source); }
Avec cette validation, tant que l’utilisateur n’aura pas saisi un email valide, il ne pourra pas passer à la prochaine étape.
Implémentation de la méthode OnTurnAsync
Passons maintenant à l’implémentation de la méthode OnTurnAsync :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 public Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); }
Pour rappel, toute conversation avec le bot appelle cette méthode. Elle prend en paramètre ITurnContext qui contient toutes les données de contexte du bot.
La première chose à faire est de vérifier si l’on a un contexte initialisé. Sinon, générer une exception :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 if (turnContext == null) { throw new ArgumentNullException(nameof(turnContext)); }
Nous devons ensuite utiliser la pile de dialogues pour initialiser le contexte du dialogue. Le Framework va utiliser le contexte du bot pour identifier l’état courant du dialogue.
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 // Run the DialogSet - let the framework identify the current state of the dialog from // the dialog stack and figure out what (if any) is the active dialog. var dialogContext = await _dialogs.CreateContextAsync(turnContext, cancellationToken); var results = await dialogContext.ContinueDialogAsync(cancellationToken);
A l’initialisation d’une conversation avec un utilisateur, le bot doit afficher un message de bienvenue et démarrer la pile des dialogues en spécifiant l’ID du dialogue qui sera exécuté en premier. A l’initialisation de la conversation, le type de l’activité est toujours ConversationUpdate. Nous devons toutefois nous assurer qu’il s’agit d’une nouvelle personne avant d’afficher à ce dernier le message de bienvenue :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 // Handle Message activity type, which is the main activity type for shown within a conversational interface // Processes ConversationUpdate Activities to welcome the user. if (turnContext.Activity.Type == ActivityTypes.ConversationUpdate) { if (turnContext.Activity.MembersAdded.Any()) { foreach (var member in turnContext.Activity.MembersAdded) { if (member.Id != turnContext.Activity.Recipient.Id) { // Sends a welcome message to the user. await SendWelcomeMessageAsync(turnContext, cancellationToken); // Pushes a new dialog onto the dialog stack. await dialogContext.BeginDialogAsync("details", null, cancellationToken); } } } }
La méthode BeginDialogAsync est utilisée pour initialiser des dialogues, ainsi que leurs propriétés. Elle doit être utilisée lorsque vous voulez démarrer une nouvelle pile de dialogues. L’identifiant du dialogue actif doit être passé en paramètre.
Les échanges avec le Bot sont de type Message. Nous devons dans ce cas exécuter le dialogue actif dans notre pile en cascade. Si aucun dialogue n’est actif, il faudra démarrer un nouveau dialogue :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13 // Message activities may contain text, speech, interactive cards, and binary or unknown attachments. // see <a href="https://aka.ms/about-bot-activity-message" target="_blank">https://aka.ms/about-bot-activity-message</a> to learn more about the message and other activity types else if (turnContext.Activity.Type == ActivityTypes.Message) { // Continues execution of the active dialog, if there is one var results = await dialogContext.ContinueDialogAsync(cancellationToken); // If the DialogTurnStatus is Empty we should start a new dialog. if(results.Status == DialogTurnStatus.Empty) { await dialogContext.BeginDialogAsync("details", null, cancellationToken); } }
Pour finir, nous devons sauvegarder l’état du dialogue dans l’état de la conversation et les informations qui ont été recueillies (FeedbackData) dans l’état de l’utilisateur :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5 // Save the dialog state into the conversation state. await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken); // Save the FeedbackData updates into the user state. await _accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
Le code complet de cette méthode est le suivant :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) { if (turnContext == null) { throw new ArgumentNullException(nameof(turnContext)); } // Run the DialogSet - let the framework identify the current state of the dialog from // the dialog stack and figure out what (if any) is the active dialog. var dialogContext = await _dialogs.CreateContextAsync(turnContext, cancellationToken); // Handle Message activity type, which is the main activity type for shown within a conversational interface // Processes ConversationUpdate Activities to welcome the user. if (turnContext.Activity.Type == ActivityTypes.ConversationUpdate) { if (turnContext.Activity.MembersAdded.Any()) { foreach (var member in turnContext.Activity.MembersAdded) { if (member.Id != turnContext.Activity.Recipient.Id) { // Sends a welcome message to the user. await SendWelcomeMessageAsync(turnContext, cancellationToken); // Pushes a new dialog onto the dialog stack. await dialogContext.BeginDialogAsync("details", null, cancellationToken); } } } } // Message activities may contain text, speech, interactive cards, and binary or unknown attachments. // see <a href="https://aka.ms/about-bot-activity-message" target="_blank">https://aka.ms/about-bot-activity-message</a> to learn more about the message and other activity types else if (turnContext.Activity.Type == ActivityTypes.Message) { // Continues execution of the active dialog, if there is one var results = await dialogContext.ContinueDialogAsync(cancellationToken); // If the DialogTurnStatus is Empty we should start a new dialog. if(results.Status == DialogTurnStatus.Empty) { await dialogContext.BeginDialogAsync("details", null, cancellationToken); } } // Save the dialog state into the conversation state. await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken); // Save the FeedbackData updates into the user state. await _accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken); }
Et le code complet de la classe RequestFeedbackDialog est le suivant :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235 public class RequestFeedbackDialog : IBot { private readonly BotAccessors _accessors; private readonly ILogger _logger; /// <summary> /// The <see cref="DialogSet"/> that contains all the Dialogs that can be used at runtime. /// </summary> private DialogSet _dialogs; public RequestFeedbackDialog(BotAccessors accessors, ILoggerFactory loggerFactory) { _accessors = accessors ?? throw new ArgumentNullException(nameof(accessors)); if (loggerFactory == null) { throw new System.ArgumentNullException(nameof(loggerFactory)); } _logger = loggerFactory.CreateLogger<RequestFeedbackDialog>(); _logger.LogTrace("RequestFeedbackDialogBot turn start."); // The DialogSet needs a DialogState accessor, it will call it when it has a turn context. _dialogs = new DialogSet(accessors.ConversationDialogState); // This array defines how the Waterfall will execute. var waterfallSteps = new WaterfallStep[] { RequestStepAsync, NameStepAsync, NameConfirmStepAsync, EmailConfirmStepAsync, SummaryStepAsync, }; // Add named dialogs to the DialogSet. These names are saved in the dialog state. _dialogs.Add(new WaterfallDialog("details", waterfallSteps)); _dialogs.Add(new ConfirmPrompt("confirm", defaultLocale: Culture.French)); _dialogs.Add(new TextPrompt("nom")); _dialogs.Add(new TextPrompt("email", EmailPromptValidatorAsync)); _dialogs.Add(new TextPrompt("message")); } /// <summary> /// Every conversation turn for our Bot will call this method. /// </summary> /// <param name="turnContext">A <see cref="ITurnContext"/> containing all the data needed /// for processing this conversation turn. </param> /// <param name="cancellationToken">(Optional) A <see cref="CancellationToken"/> that can be used by other objects /// or threads to receive notice of cancellation.</param> /// <returns>A <see cref="Task"/> that represents the work queued to execute.</returns> /// <seealso cref="BotStateSet"/> /// <seealso cref="ConversationState"/> /// <seealso cref="IMiddleware"/> public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) { if (turnContext == null) { throw new ArgumentNullException(nameof(turnContext)); } // Run the DialogSet - let the framework identify the current state of the dialog from // the dialog stack and figure out what (if any) is the active dialog. var dialogContext = await _dialogs.CreateContextAsync(turnContext, cancellationToken); // Handle Message activity type, which is the main activity type for shown within a conversational interface // Processes ConversationUpdate Activities to welcome the user. if (turnContext.Activity.Type == ActivityTypes.ConversationUpdate) { if (turnContext.Activity.MembersAdded.Any()) { foreach (var member in turnContext.Activity.MembersAdded) { if (member.Id != turnContext.Activity.Recipient.Id) { // Sends a welcome message to the user. await SendWelcomeMessageAsync(turnContext, cancellationToken); // Pushes a new dialog onto the dialog stack. await dialogContext.BeginDialogAsync("details", null, cancellationToken); } } } } // Message activities may contain text, speech, interactive cards, and binary or unknown attachments. // see https://aka.ms/about-bot-activity-message to learn more about the message and other activity types else if (turnContext.Activity.Type == ActivityTypes.Message) { // Continues execution of the active dialog, if there is one var results = await dialogContext.ContinueDialogAsync(cancellationToken); // If the DialogTurnStatus is Empty we should start a new dialog. if(results.Status == DialogTurnStatus.Empty) { await dialogContext.BeginDialogAsync("details", null, cancellationToken); } } // Save the dialog state into the conversation state. await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken); // Save the FeedbackData updates into the user state. await _accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken); } private static async Task SendWelcomeMessageAsync(ITurnContext turnContext, CancellationToken cancellationToken) { await turnContext.SendActivityAsync( "Bienvenue. Ce bot permet de recueillir vos avis.", cancellationToken: cancellationToken); } /// <summary> /// One of the functions that make up the <see cref="WaterfallDialog"/>. /// </summary> /// <param name="stepContext">The <see cref="WaterfallStepContext"/> gives access to the executing dialog runtime.</param> /// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param> /// <returns>A <see cref="DialogTurnResult"/> to communicate some flow control back to the containing WaterfallDialog.</returns> private async Task<DialogTurnResult> RequestStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. return await stepContext.PromptAsync("confirm", new PromptOptions { Prompt = MessageFactory.Text("Voulez-vous nous laisser vos commentaires ?") }, cancellationToken); } /// <summary> /// One of the functions that make up the <see cref="WaterfallDialog"/>. /// </summary> /// <param name="stepContext">The <see cref="WaterfallStepContext"/> gives access to the executing dialog runtime.</param> /// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param> /// <returns>A <see cref="DialogTurnResult"/> to communicate some flow control back to the containing WaterfallDialog.</returns> private async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { if ((bool)stepContext.Result) { // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. // Running a prompt here means the next WaterfallStep will be run when the users response is received. return await stepContext.PromptAsync("nom", new PromptOptions { Prompt = MessageFactory.Text("Veuillez saisir votre nom.") }, cancellationToken); } else { await stepContext.Context.SendActivityAsync(MessageFactory.Text("Merci. Aurevoir."), cancellationToken); return await stepContext.EndDialogAsync(cancellationToken: cancellationToken); } } /// <summary> /// One of the functions that make up the <see cref="WaterfallDialog"/>. /// </summary> /// <param name="stepContext">The <see cref="WaterfallStepContext"/> gives access to the executing dialog runtime.</param> /// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param> /// <returns>A <see cref="DialogTurnResult"/> to communicate some flow control back to the containing WaterfallDialog.</returns> private async Task<DialogTurnResult> NameConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { // Get the current feedbackData object from user state. var feedbackData = await _accessors.FeedbackData.GetAsync(stepContext.Context, () => new FeedbackData(), cancellationToken); // Update the feedbackData. feedbackData.Name = (string)stepContext.Result; // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. return await stepContext.PromptAsync("email", new PromptOptions { Prompt = MessageFactory.Text("Veuillez saisir votre adresse email.") }, cancellationToken); } /// <summary> /// One of the functions that make up the <see cref="WaterfallDialog"/>. /// </summary> /// <param name="stepContext">The <see cref="WaterfallStepContext"/> gives access to the executing dialog runtime.</param> /// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param> /// <returns>A <see cref="DialogTurnResult"/> to communicate some flow control back to the containing WaterfallDialog.</returns> private async Task<DialogTurnResult> EmailConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { // Get the current feedbackData object from user state. var feedbackData = await _accessors.FeedbackData.GetAsync(stepContext.Context, () => new FeedbackData(), cancellationToken); // Update the feedbackData. feedbackData.Email = (string)stepContext.Result; // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. return await stepContext.PromptAsync("message", new PromptOptions { Prompt = MessageFactory.Text("Veuillez saisir votre message.") }, cancellationToken); } /// <summary> /// One of the functions that make up the <see cref="WaterfallDialog"/>. /// </summary> /// <param name="stepContext">The <see cref="WaterfallStepContext"/> gives access to the executing dialog runtime.</param> /// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param> /// <returns>A <see cref="DialogTurnResult"/> to communicate some flow control back to the containing WaterfallDialog.</returns> private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { // Get the current feedbackData object from user state. var feedbackData = await _accessors.FeedbackData.GetAsync(stepContext.Context, () => new FeedbackData(), cancellationToken); // Update the feedbackData. feedbackData.Message = (string)stepContext.Result; await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Votre nom {feedbackData.Name}, votre email {feedbackData.Email} et votre message {feedbackData.Message}."), cancellationToken); // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is the end. return await stepContext.EndDialogAsync(cancellationToken: cancellationToken); } /// <summary> /// This is an example of a custom validator. /// Returning true indicates the recognized value is acceptable. Returning false will trigger re-prompt behavior. /// </summary> /// <param name="promptContext">The <see cref="PromptValidatorContext"/> gives the validator code access to the runtime, including the recognized value and the turn context.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A <see cref="Task"/> representing the operation result of the Turn operation.</returns> private Task<bool> EmailPromptValidatorAsync(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken) { var result = promptContext.Recognized.Value; // This condition is our validation rule. if (IsValidEmail(result)) { // Success is indicated by passing back the value the Prompt has collected. You must pass back a value even if you haven't changed it. return Task.FromResult(true); } // Not calling End indicates validation failure. This will trigger a RetryPrompt if one has been defined. return Task.FromResult(false); } private bool IsValidEmail(string source) { return new EmailAddressAttribute().IsValid(source); } }
Mise à jour du Startup
Dans la méthode ConfigureServices, vous devez remplacer :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part services.AddBot<EchoWithCounterBot>
Par
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part services.AddBot<RequestFeedbackDialog>
Ensuite, à la fin de la section de configuration du bot, où l’on procède à la création de l’état de la conversation :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 var conversationState = new ConversationState(dataStore); options.State.Add(conversationState);
vous devez aussi procéder à la création de l’état de l’utilisateur :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 var conversationState = new ConversationState(dataStore); options.State.Add(conversationState); // Create and add user state. var userState = new UserState(dataStore); options.State.Add(userState);
Pour finir, vous devez remplacer :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 services.AddSingleton<EchoBotAccessors>(sp => { var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value; if (options == null) { throw new InvalidOperationException("BotFrameworkOptions must be configured prior to setting up the state accessors"); } var conversationState = options.State.OfType<ConversationState>().FirstOrDefault(); if (conversationState == null) { throw new InvalidOperationException("ConversationState must be defined and added before adding conversation-scoped state accessors."); } // Create the custom state accessor. // State accessors enable other components to read and write individual properties of state. var accessors = new EchoBotAccessors(conversationState) { CounterState = conversationState.CreateProperty<CounterState>(EchoBotAccessors.CounterStateName), }; return accessors; });
Par :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 services.AddSingleton<BotAccessors>(sp => { // We need to grab the conversationState we added on the options in the previous step var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value; if (options == null) { throw new InvalidOperationException("BotFrameworkOptions must be configured prior to setting up the State Accessors"); } var conversationState = options.State.OfType<ConversationState>().FirstOrDefault(); if (conversationState == null) { throw new InvalidOperationException("ConversationState must be defined and added before adding conversation-scoped state accessors."); } var userState = options.State.OfType<UserState>().FirstOrDefault(); if (userState == null) { throw new InvalidOperationException("UserState must be defined and added before adding user-scoped state accessors."); } // Create the custom state accessor. // State accessors enable other components to read and write individual properties of state. var accessors = new BotAccessors(conversationState, userState) { ConversationDialogState = conversationState.CreateProperty<DialogState>("DialogState"), FeedbackData = userState.CreateProperty<FeedbackData>("FeedbackData"), }; return accessors; });
Vous remarquerez que c’est à initialisation de l’accesseur que nous créons nos propres états personnalisés (ConversationDialogState et FeedbackData). L’état du dialogue doit être lié à l’état de la conversation, tandis que le FeedbackData doit être lié à l’état de l’utilisateur.
Enregistrez vos modifications et exécutez votre bot.
Vous obtiendrez le résultat suivant :
J’espère que ce billet vous sera utile dans vos premiers pas avec le Bot Builder SDK V4.
Restez connecté pour d’autres billets sur le Bot Framework.
Le code complet de cet exemple sur mon GitHub
Démarrer avec le Bot Builder SDK V4 pour .NET
Bot Framework : exploiter les fonctionnalités du SDK V3 dans la version 4 du Bot Builder
Documentation officielle du SDK V4
GitHub du Bot Builder SDK V4
GitHub du Bot Framework Emulator V4
Blog Bot Framework