diff --git a/CSharp/Library/Dialogs/LuisDialog.cs b/CSharp/Library/Dialogs/LuisDialog.cs index 1f0f2cc22..f87006398 100644 --- a/CSharp/Library/Dialogs/LuisDialog.cs +++ b/CSharp/Library/Dialogs/LuisDialog.cs @@ -74,17 +74,26 @@ namespace Microsoft.Bot.Builder public Models.EntityRecommendation[] Entities { get; set; } } + public static partial class Extensions + { + public static bool TryFindEntity(this LuisResult result, string type, out Models.EntityRecommendation entity) + { + entity = result.Entities?.FirstOrDefault(e => e.Type == type); + return entity != null; + } + } + public delegate Task IntentHandler(IDialogContext context, LuisResult luisResult); [Serializable] - public class LuisDialog : IDialog, ISerializable + public class LuisDialog : IDialog { public readonly string subscriptionKey; public readonly string modelID; public readonly string luisUrl; - protected readonly Dictionary handlerByIntent = new Dictionary(); - protected const string DefaultIntentHandler = "87DBD4FD7736"; + [NonSerialized] + protected Dictionary handlerByIntent; public LuisDialog() { @@ -109,22 +118,6 @@ namespace Microsoft.Bot.Builder this.luisUrl = string.Format("https://api.projectoxford.ai/luis/v1/application?id={0}&subscription-key={1}&q=", this.modelID, this.subscriptionKey); } - protected LuisDialog(SerializationInfo info, StreamingContext context) - { - Field.SetNotNullFrom(out this.luisUrl, nameof(this.luisUrl), info); - Field.SetFrom(out this.subscriptionKey, nameof(this.subscriptionKey), info); - Field.SetFrom(out this.modelID, nameof(this.modelID), info); - - this.AddAttributeBasedHandlers(); - } - - public virtual void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(this.subscriptionKey), this.subscriptionKey); - info.AddValue(nameof(this.luisUrl), this.luisUrl); - info.AddValue(nameof(this.modelID), this.modelID); - } - public virtual async Task StartAsync(IDialogContext context) { context.Wait(MessageReceived); @@ -149,10 +142,16 @@ namespace Microsoft.Bot.Builder var message = await item; var luisRes = await GetLuisResult(this.luisUrl, message.Text); var intent = luisRes.Intents.FirstOrDefault(i => i.Score == luisRes.Intents.Select(t => t.Score).Max()); + + if (this.handlerByIntent == null) + { + this.AddAttributeBasedHandlers(); + } + IntentHandler handler; if (intent == null || !this.handlerByIntent.TryGetValue(intent.Intent, out handler)) { - handler = this.handlerByIntent[DefaultIntentHandler]; + handler = this.handlerByIntent[string.Empty]; } if (handler != null) @@ -168,6 +167,13 @@ namespace Microsoft.Bot.Builder private void AddAttributeBasedHandlers() { + if (this.handlerByIntent != null) + { + throw new InvalidOperationException(); + } + + this.handlerByIntent = new Dictionary(); + var methods = from m in this.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) let attr = m.GetCustomAttributes(typeof(LuisIntent), true) where attr.Length > 0 @@ -191,7 +197,7 @@ namespace Microsoft.Bot.Builder foreach (var intent in handler.intents) { - var key = string.IsNullOrEmpty(intent) ? DefaultIntentHandler : intent; + var key = string.IsNullOrWhiteSpace(intent) ? string.Empty : intent; this.handlerByIntent.Add(key, intentHandler); } } diff --git a/CSharp/Samples/PizzaBot/PizzaOrderDialog.cs b/CSharp/Samples/PizzaBot/PizzaOrderDialog.cs index 379f1ada0..a40d6fcbd 100644 --- a/CSharp/Samples/PizzaBot/PizzaOrderDialog.cs +++ b/CSharp/Samples/PizzaBot/PizzaOrderDialog.cs @@ -22,18 +22,6 @@ namespace Microsoft.Bot.Sample.PizzaBot this.MakePizzaForm = makePizzaForm; } - protected PizzaOrderDialog(SerializationInfo info, StreamingContext context) - : base(info, context) - { - Field.SetNotNullFrom(out this.MakePizzaForm, nameof(MakePizzaForm), info); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - info.AddValue(nameof(this.MakePizzaForm), MakePizzaForm); - } - [LuisIntent("")] public async Task None(IDialogContext context, LuisResult result) { diff --git a/CSharp/Samples/SimpleAlarmBot/Microsoft.Bot.Sample.SimpleAlarmBot.csproj b/CSharp/Samples/SimpleAlarmBot/Microsoft.Bot.Sample.SimpleAlarmBot.csproj index 19c23cbb3..0e3d8075a 100644 --- a/CSharp/Samples/SimpleAlarmBot/Microsoft.Bot.Sample.SimpleAlarmBot.csproj +++ b/CSharp/Samples/SimpleAlarmBot/Microsoft.Bot.Sample.SimpleAlarmBot.csproj @@ -1,176 +1,180 @@ - - - - - - Debug - AnyCPU - - - 2.0 - {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4} - {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} - Library - Properties - Microsoft.Bot.Sample.SimpleAlarmBot - Microsoft.Bot.Sample.SimpleAlarmBot - v4.6 - true - - - - - - - - - - - true - full - false - bin\ - DEBUG;TRACE - prompt - 4 - CS1998 - - - pdbonly - true - bin\ - TRACE - prompt - 4 - - - - ..\..\packages\Microsoft.Bot.Connector.1.0.0.0\lib\net45\Microsoft.Bot.Connector.dll - True - - - ..\..\packages\Microsoft.Bot.Connector.1.0.0.0\lib\net45\Microsoft.Bot.Connector.Utilities.dll - True - - - - ..\..\packages\Microsoft.Diagnostics.Tracing.EventSource.Redist.1.1.28\lib\net46\Microsoft.Diagnostics.Tracing.EventSource.dll - True - - - ..\..\packages\Microsoft.Rest.ClientRuntime.1.8.2\lib\net45\Microsoft.Rest.ClientRuntime.dll - True - - - ..\..\packages\Microsoft.WindowsAzure.ConfigurationManager.3.2.1\lib\net40\Microsoft.WindowsAzure.Configuration.dll - True - - - ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - True - - - - - - ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll - True - - - - - - - - - - - - - - ..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll - True - - - ..\..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.3\lib\net45\System.Web.Http.WebHost.dll - True - - - - - - - - - - - - Designer - - - - - - - Global.asax - - - - - + + + + + + Debug + AnyCPU + + + 2.0 + {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + Microsoft.Bot.Sample.SimpleAlarmBot + Microsoft.Bot.Sample.SimpleAlarmBot + v4.6 + true + + + + + + + + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + CS1998 + + + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + ..\..\packages\Chronic.0.3.1\lib\net40\Chronic.dll + True + + + ..\..\packages\Microsoft.Bot.Connector.1.0.0.0\lib\net45\Microsoft.Bot.Connector.dll + True + + + ..\..\packages\Microsoft.Bot.Connector.1.0.0.0\lib\net45\Microsoft.Bot.Connector.Utilities.dll + True + + + + ..\..\packages\Microsoft.Diagnostics.Tracing.EventSource.Redist.1.1.28\lib\net46\Microsoft.Diagnostics.Tracing.EventSource.dll + True + + + ..\..\packages\Microsoft.Rest.ClientRuntime.1.8.2\lib\net45\Microsoft.Rest.ClientRuntime.dll + True + + + ..\..\packages\Microsoft.WindowsAzure.ConfigurationManager.3.2.1\lib\net40\Microsoft.WindowsAzure.Configuration.dll + True + + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True + + + + + + ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll + True + + + + + + + + + + + + + + ..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll + True + + + ..\..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.3\lib\net45\System.Web.Http.WebHost.dll + True + + + + + + + + + + + + Designer + + + + + + + Global.asax + + + + + Designer - - Web.config - - - Web.config - Designer - - - - - {cdfec7d6-847e-4c13-956b-0a960ae3eb60} - Microsoft.Bot.Builder - - - - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - - - - - True - True - 3978 - / - http://localhost:3979/ - False - False - - - False - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + Web.config + + + Web.config + Designer + + + + + {cdfec7d6-847e-4c13-956b-0a960ae3eb60} + Microsoft.Bot.Builder + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + + True + True + 3978 + / + http://localhost:3979/ + False + False + + + False + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + --> \ No newline at end of file diff --git a/CSharp/Samples/SimpleAlarmBot/SimpleAlarmBot.cs b/CSharp/Samples/SimpleAlarmBot/SimpleAlarmBot.cs index 8f322e725..08fd107fb 100644 --- a/CSharp/Samples/SimpleAlarmBot/SimpleAlarmBot.cs +++ b/CSharp/Samples/SimpleAlarmBot/SimpleAlarmBot.cs @@ -1,97 +1,192 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Models; +using Newtonsoft.Json.Linq; namespace Microsoft.Bot.Sample.SimpleAlarmBot { [LuisModel("https://api.projectoxford.ai/luis/v1/application?id=c413b2ef-382c-45bd-8ff0-f76d60e2a821&subscription-key=fe054e042fd14754a83f0a205f6552a5&q=")] [Serializable] - public class SimpleAlarmBot : LuisDialog + public class SimpleAlarmBot : LuisDialog, ISerializable { - public SimpleAlarmBot() + private readonly List alarms = new List(); + + public const string DefaultAlarmWhat = "default"; + + public bool TryFindAlarm(LuisResult result, out Alarm alarm) { + alarm = null; + + string what; + + EntityRecommendation title; + if (result.TryFindEntity(Entity_Alarm_Title, out title)) + { + what = title.Entity; + } + else + { + what = DefaultAlarmWhat; + } + + alarm = this.alarms.FirstOrDefault(a => a.What == what); + return alarm != null; } - protected SimpleAlarmBot(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } + private const string Entity_Alarm_Title = "builtin.alarm.title"; + private const string Entity_Alarm_Start_Time = "builtin.alarm.start_time"; + private const string Entity_Alarm_Start_Date = "builtin.alarm.start_date"; - /// - /// default intent handler because it is marked by [LuisIntent("")] - /// - /// - /// - /// [LuisIntent("")] public async Task None(IDialogContext context, LuisResult result) { - await context.PostAsync("I'm sorry. I didn't understand you."); - context.Wait(MessageReceived); - } - - [LuisIntent("builtin.intent.alarm.alarm_other")] - public async Task AlarmOther(IDialogContext context, LuisResult result) - { - string reply = "alarm changed!"; - await context.PostAsync(reply); + string message = $"Sorry I did not understand: " + string.Join(", ", result.Intents.Select(i => i.Intent)); + await context.PostAsync(message); context.Wait(MessageReceived); } [LuisIntent("builtin.intent.alarm.delete_alarm")] public async Task DeleteAlarm(IDialogContext context, LuisResult result) { - string reply = "alarm deleted!"; - await context.PostAsync(reply); + Alarm alarm; + if (TryFindAlarm(result, out alarm)) + { + this.alarms.Remove(alarm); + await context.PostAsync($"alarm {alarm} deleted"); + } + else + { + await context.PostAsync("did not find alarm"); + } + context.Wait(MessageReceived); } [LuisIntent("builtin.intent.alarm.find_alarm")] public async Task FindAlarm(IDialogContext context, LuisResult result) { - string reply = "alarm found!"; - await context.PostAsync(reply); + Alarm alarm; + if (TryFindAlarm(result, out alarm)) + { + await context.PostAsync($"found alarm {alarm}"); + } + else + { + await context.PostAsync("did not find alarm"); + } + context.Wait(MessageReceived); } [LuisIntent("builtin.intent.alarm.set_alarm")] public async Task SetAlarm(IDialogContext context, LuisResult result) { - string reply = "alarm created!"; - await context.PostAsync(reply); + EntityRecommendation title; + if (! result.TryFindEntity(Entity_Alarm_Title, out title)) + { + title = new EntityRecommendation(Entity_Alarm_Title) { Entity = DefaultAlarmWhat }; + } + + EntityRecommendation date; + if (! result.TryFindEntity(Entity_Alarm_Start_Date, out date)) + { + date = new EntityRecommendation() { Entity = string.Empty }; + } + + EntityRecommendation time; + if (!result.TryFindEntity(Entity_Alarm_Start_Time, out time)) + { + time = new EntityRecommendation() { Entity = string.Empty }; + } + + var parser = new Chronic.Parser(); + var span = parser.Parse(date.Entity + " " + time.Entity); + + if (span != null) + { + var when = span.Start ?? span.End; + var alarm = new Alarm() { What = title.Entity, When = when.Value }; + this.alarms.Add(alarm); + + string reply = $"alarm {alarm} created"; + await context.PostAsync(reply); + } + else + { + await context.PostAsync("could not find time for alarm"); + } + context.Wait(MessageReceived); } [LuisIntent("builtin.intent.alarm.snooze")] public async Task AlarmSnooze(IDialogContext context, LuisResult result) { - string reply = "alarm snoozed!"; - await context.PostAsync(reply); + Alarm alarm; + if (TryFindAlarm(result, out alarm)) + { + alarm.When = alarm.When.Add(TimeSpan.FromMinutes(7)); + await context.PostAsync($"alarm {alarm} snoozed!"); + } + else + { + await context.PostAsync("did not find alarm"); + } + context.Wait(MessageReceived); } [LuisIntent("builtin.intent.alarm.time_remaining")] public async Task TimeRemaining(IDialogContext context, LuisResult result) { - string reply = "There are 5 minutes remaining."; - await context.PostAsync(reply); + Alarm alarm; + if (TryFindAlarm(result, out alarm)) + { + var now = DateTime.UtcNow; + if (alarm.When > now) + { + var remaining = alarm.When.Subtract(DateTime.UtcNow); + await context.PostAsync($"There is {remaining} remaining for alarm {alarm}."); + } + else + { + await context.PostAsync($"The alarm {alarm} expired already."); + } + } + else + { + await context.PostAsync("did not find alarm"); + } + context.Wait(MessageReceived); } + private Alarm turnOff; + [LuisIntent("builtin.intent.alarm.turn_off_alarm")] public async Task TurnOffAlarm(IDialogContext context, LuisResult result) { - Prompts.Confirm(context, AfterConfirming_TurnOffAlarm, "Are you sure?"); + if (TryFindAlarm(result, out this.turnOff)) + { + Prompts.Confirm(context, AfterConfirming_TurnOffAlarm, "Are you sure?"); + } + else + { + await context.PostAsync("did not find alarm"); + } } public async Task AfterConfirming_TurnOffAlarm(IDialogContext context, IAwaitable confirmation) { if (await confirmation) { - await context.PostAsync("Ok, alarm disabled."); + this.alarms.Remove(this.turnOff); + await context.PostAsync($"Ok, alarm {this.turnOff} disabled."); } else { @@ -100,5 +195,58 @@ namespace Microsoft.Bot.Sample.SimpleAlarmBot context.Wait(MessageReceived); } + + [LuisIntent("builtin.intent.alarm.alarm_other")] + public async Task AlarmOther(IDialogContext context, LuisResult result) + { + await context.PostAsync("what ?"); + context.Wait(MessageReceived); + } + + public SimpleAlarmBot() + { + } + + protected SimpleAlarmBot(SerializationInfo info, StreamingContext context) + { + var json = info.GetValue(nameof(this.alarms)); + this.alarms = JArray.Parse(json).ToObject>(); + this.turnOff = info.GetValue(nameof(this.turnOff)); + } + + void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(this.alarms), JArray.FromObject(this.alarms).ToString()); + info.AddValue(nameof(this.turnOff), turnOff); + } + + [Serializable] + public sealed class Alarm : IEquatable + { + public DateTime When { get; set; } + public string What { get; set; } + + public override string ToString() + { + return $"[{this.What} at {this.When}]"; + } + + public bool Equals(Alarm other) + { + return other != null + && this.When == other.When + && this.What == other.What; + } + + public override bool Equals(object other) + { + return Equals(other as Alarm); + } + + public override int GetHashCode() + { + return this.What.GetHashCode(); + } + } } } \ No newline at end of file diff --git a/CSharp/Samples/SimpleAlarmBot/packages.config b/CSharp/Samples/SimpleAlarmBot/packages.config index b6feb998a..52617d4a4 100644 --- a/CSharp/Samples/SimpleAlarmBot/packages.config +++ b/CSharp/Samples/SimpleAlarmBot/packages.config @@ -1,5 +1,6 @@  +