Für meine Präsentation bei Sommer, Sonne, Bots habe ich eine Demo, den Rocket Commander Bot (mehr dazu in einem eigenen Beitrag), vorbereitet. Mit dem Bot ist es möglich, per Sprachbefehlen über Cortana eine (Silvester-)Rakete zu starten.
Ausgangssituation
Um mit dem Rocket Commander Bot eine Rakete zu starten, wollte ich, dass der Bot mehrere Parameter abfragt, die für den Raketenstart notwendig sind. Dazu zählen das Ziel, die Startzeit und ein geheimer Startcode. Das Bot Builder SDK enthält bereits ein Werkzeug, das genau dieses Anwendungsfall abdeckt: FormFlow generierte Dialoge.
Der FormFlow Dialog ist schnell implementiert und sieht wie folgt aus:
[Serializable] public class LaunchQuery { [Prompt("Where do you want to go?")] public string Destination { get; set; } [Prompt("When should I launch it?")] public DateTime? LaunchDateTime { get; set; } [Prompt("Please give me the super secret launch code?")] public string LaunchCode { get; set; } public static IForm<LaunchQuery> BuildLaunchForm() { return new FormBuilder<LaunchQuery>() .AddRemainingFields() .Build(); } }
Gestartet wird der Dialog wenn der LaunchRocket Intent aufgerufen wird.
public class RootDialog : LuisDialog<object> { [LuisIntent("LaunchRocket")] public async Task LaunchRocketIntent(IDialogContext context, LuisResult result) { var launchQuery = new LaunchQuery(); var launchFormDialog = new FormDialog<LaunchQuery>(launchQuery, LaunchQuery.BuildLaunchForm, FormOptions.PromptInStart); context.Call(launchFormDialog, this.ResumeAfterLaunchFormDialog); } private async Task ResumeAfterLaunchFormDialog(IDialogContext context, IAwaitable<LaunchQuery> result) { var queryResult = await result; var message = context.MakeMessage(); message.Text = "Rocket launch initiated"; await context.PostAsync(message); // launch rocket context.Wait(MessageReceived); } }
Im wesentlichen tut der Bot damit auch schon genau das, was er soll. Zumindest trifft das zu, wenn man den Bot im Emulator oder auf einer anderen Plattform mit Texteingabe verwendet. Bindet man aber Cortana an und erwartet, dass sie mit einem Spricht und Fragen stellt, wird man leider enttäuscht.
Problem
Um den Dialog mit Cortana zu starten, spricht man den Befehl „Hey Cortana, ask Rocket Commander to launch the rocket“.
Wie man am Screenshot erkennen kann, funktioniert der Dialog grundsätzlich und Cortana fragt sofort nach dem Ziel. Doch es gibt zwei Probleme: Erstens stellt Cortana die Frage nicht gesprochen sondern nur als Text und zweitens kann man nicht einfach los sprechen um die Frage zu beantwortet, sondern muss zuerst mit der Maus auf die Eingabebox klicken.
Sprachunterstützung im Bot Framework
Das Bot Framework bzw. Das Bot Builder SDK hat erst vor kurzem Unterstützung für Sprache bekommen.
Dazu wurden der IMessageActivity zwei Felder hinzugefügt: Speak und InputHint. Das Feld Speak kann mit einfachem Text oder aber auch mit SSML (Speech Synthesis Markup Language) befüllt werden. Der Bot, in unserem Fall Cortana, nutzt diese Informationen um Sprache auszugeben. Das zweite Feld InputHint kann auf drei verschiedene Werte gesetzt werden AcceptingInput, ExpectingInput und IgnorningInput. Der erste Wert gibt an, dass eine Antwort möglich ist und der Benutzer von sich aus das Mikrofon aktivieren und antworten kann. Dies ist auch der Standardwert für den vorhin genannten Fall, bei dem ich das Mikrofon durch einen Klick auf die Eingabebox selbst aktivieren musste. Ist der zweite Wert gesetzt wird das Mikro automatisch aktiviert und man kann sofort antworten. Beim dritten Wert ist keine Antwort möglich und man kann das Mikro auch nicht selbst aktivieren.
Lösung
Mit diesen neuen Feldern lassen sich also die vorhin genannten Probleme lösen. Doch wie kann man diese Felder setzen, wenn man einen FormFlow generierten Dialog verwendet? Leider gibt es derzeit noch keine Sprachunterstützung dafür. Und da FormFlow im Hintergrund sehr viel „Magic“ verwendet um die Dialoge zu generieren, kann man da auch nicht ohne weiteres eingreifen.
Glücklicherweise bietet das Bot Builder SDK einige andere Punkte, an denen man sich einklinken und den Ablauf beeinflussen kann. Eine dieser Möglichkeiten ist eine eigene Implementierung des IMessageActivityMapper Interfaces. Dazu konnte ich auch gleich eine Implementierung auf Stackoverflow finden, die ich nur noch leicht anpassen musste.
So sieht die fertige Lösung aus:
public sealed class TextToSpeakActivityMapper : IMessageActivityMapper { public IMessageActivity Map(IMessageActivity message) { // only set the speak if it is not set by the developer. var channelCapability = new ChannelCapability(Address.FromActivity(message)); if (channelCapability.SupportsSpeak() && string.IsNullOrEmpty(message.Speak)) { message.Speak = message.Text; // set InputHint to ExpectingInput if text is a question var isQuestion = message.Text?.EndsWith("?"); if (isQuestion.GetValueOrDefault()) { message.InputHint = InputHints.ExpectingInput; } } return message; } }
Dabei wird jede ausgehende MessageActivity überprüft und wenn der Kanal Sprache unterstützt und das Speak Feld noch nicht gesetzt ist, wird der gesendete Text auch für die Sprachausgabe verwendet. Ich habe dann nur noch ergänzt, dass der InputHint auf ExpectingInput gesetzt wird, wenn es sich um eine Frage handelt.
Registriert wird der TextToSpeakActivityMapper in der Global.asax (oder in der Startup.cs, wenn man ASP.NET Core verwendet):
public class WebApiApplication : System.Web.HttpApplication { protected void Application_Start() { var builder = new ContainerBuilder(); builder .RegisterType<TextToSpeakActivityMapper>() .AsImplementedInterfaces() .SingleInstance(); builder.Update(Conversation.Container); GlobalConfiguration.Configure(WebApiConfig.Register); } }
Fazit
Diese Lösung ist natürlich nicht perfekt und das Verhalten wird unter Umständen nicht in allen Fällen so gewünscht sein, aber für meinen Rocket Commander Bot ist es ausreichend und ich hoffe es hilft auch anderen, bis eine offizielle Sprachunterstützung für FormFlow generierte Dialog verfügbar ist.