diff --git a/.gitignore b/.gitignore
index 7fdde1315c..0601da8b77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -332,6 +332,5 @@ ASALocalRun/
*.bot
# AppSettings
-/solutions/Virtual-Assistant/src/csharp/assistant/appsettings.development.json
-/solutions/Virtual-Assistant/src/csharp/assistant/appsettings.production.json
-
+**/appsettings.production.json
+**/appsettings.development.json
diff --git a/solutions/Virtual-Assistant/docs/virtualassistant-createvirtualassistant.md b/solutions/Virtual-Assistant/docs/virtualassistant-createvirtualassistant.md
index 559ae8f8d6..ee81d9b611 100644
--- a/solutions/Virtual-Assistant/docs/virtualassistant-createvirtualassistant.md
+++ b/solutions/Virtual-Assistant/docs/virtualassistant-createvirtualassistant.md
@@ -52,7 +52,7 @@ Once the Solution has been cloned you will see the following folder structure.
### Build the Solution
-Once cloned the next step it to build the VirtualAssistant solution within Visual Studio. Deployment must have been completed before you can run the project due to this stage creating key dependencies in Azure along with your configured .bot file.
+Once cloned the next step is to build the VirtualAssistant solution within Visual Studio. Deployment must have been completed before you can run the project due to this stage creating key dependencies in Azure along with your configured .bot file.
### Deployment
@@ -138,7 +138,7 @@ The [Add Authentication to your bot](https://docs.microsoft.com/en-us/azure/bot-
- Leave Logout URL blank.
- Under Microsoft Graph Permissions, you can need to add additional *delegated* permissions.
- Each of the Skills require a specific set of Scopes, refer to the documentation for each skill or use the following list of Scopes that contain the scopes needed for all skills.
- - `Calendars.ReadWrite`, `Mail.Read`, `Mail.Send`, `Notes.ReadWrite`, `People.Read`, `User.Read`
+ - `Calendars.ReadWrite`, `Mail.Read`, `Mail.Send`, `Notes.ReadWrite.All`, `People.Read.All`, `User.Read.All`
Next you need to create the Authentication Connection for your Bot. Ensure you use the same combination of Scopes that you provided in the above command. The first command shown below will retrieve the appId (ApplicationId) and appPassword (Client Secret) that you need to complete this step.
@@ -147,7 +147,7 @@ The commands shown below assume you have used the deployment process and your re
```shell
msbot get production --secret YOUR_SECRET
-az bot authsetting create --resource-group YOUR_BOT_NAME --name YOUR_BOT_NAME --setting-name "YOUR_AUTH_CONNECTION_NAME" --client-id "YOUR_APPLICATION_ID" --client-secret "YOUR_APPLICATION_PASSWORD" --provider-scope-string "Calendars.ReadWrite Mail.Read Mail.Send Notes.ReadWrite People.Read User.Read" --service Aadv2
+az bot authsetting create --resource-group YOUR_BOT_NAME --name YOUR_BOT_NAME --setting-name "YOUR_AUTH_CONNECTION_NAME" --client-id "YOUR_APPLICATION_ID" --client-secret "YOUR_APPLICATION_PASSWORD" --provider-scope-string "Calendars.ReadWrite Mail.Read Mail.Send Notes.ReadWrite.All People.Read.All User.Read.All" --service Aadv2
```
The final step is to update your .bot file and associated Skills (in appSettings.config) with the Authentication connection name, this is used by the Assistant to enable Authentication prompts or use of Linked Accounts.
diff --git a/solutions/Virtual-Assistant/src/csharp/VirtualAssistant.sln b/solutions/Virtual-Assistant/src/csharp/VirtualAssistant.sln
index 1fc4b303c5..0acff829f6 100644
--- a/solutions/Virtual-Assistant/src/csharp/VirtualAssistant.sln
+++ b/solutions/Virtual-Assistant/src/csharp/VirtualAssistant.sln
@@ -13,15 +13,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualAssistant", "assista
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Solutions", "Microsoft.Bot.Solutions\Microsoft.Bot.Solutions.csproj", "{0106AD2E-C570-42DF-B54B-3ACA50D58D80}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CalendarSkill", "skills\calendarskill\CalendarSkill.csproj", "{2DF1C042-7544-4885-AFE4-E7010369A519}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Skills", "Skills", "{1F423A0A-64D9-400C-B2B0-3AC378A5AB02}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EmailSkill", "skills\emailskill\EmailSkill.csproj", "{ACB52D46-6553-44EF-89AE-D3038406FA1E}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CalendarSkill", "skills\CalendarSkill\CalendarSkill.csproj", "{3DA6F9E0-BE9F-4F16-8A41-A68948A01467}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PointOfInterestSkill", "skills\pointofinterestskill\PointOfInterestSkill.csproj", "{6BD03130-4931-410F-83EB-03E37E17E2B9}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EmailSkill", "skills\EmailSkill\EmailSkill.csproj", "{F8A8F3D9-4C7E-442A-8ADF-F0C8586F41B7}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ToDoSkill", "skills\ToDoSkill\ToDoSkill.csproj", "{3DE6A4B2-9241-4605-9E8C-BD17A1F71502}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PointOfInterestSkill", "skills\PointOfInterestSkill\PointOfInterestSkill.csproj", "{CB8CB4AF-B10B-4F0D-813F-5F9DA23B49FA}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Skills", "Skills", "{5613E456-DD6D-4A16-AB48-D2F51CAC0526}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ToDoSkill", "skills\ToDoSkill\ToDoSkill.csproj", "{73DCD314-D2DE-452D-8B7A-9DAA964F0DCD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -37,31 +37,31 @@ Global
{0106AD2E-C570-42DF-B54B-3ACA50D58D80}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0106AD2E-C570-42DF-B54B-3ACA50D58D80}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0106AD2E-C570-42DF-B54B-3ACA50D58D80}.Release|Any CPU.Build.0 = Release|Any CPU
- {2DF1C042-7544-4885-AFE4-E7010369A519}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2DF1C042-7544-4885-AFE4-E7010369A519}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2DF1C042-7544-4885-AFE4-E7010369A519}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2DF1C042-7544-4885-AFE4-E7010369A519}.Release|Any CPU.Build.0 = Release|Any CPU
- {ACB52D46-6553-44EF-89AE-D3038406FA1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {ACB52D46-6553-44EF-89AE-D3038406FA1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {ACB52D46-6553-44EF-89AE-D3038406FA1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {ACB52D46-6553-44EF-89AE-D3038406FA1E}.Release|Any CPU.Build.0 = Release|Any CPU
- {6BD03130-4931-410F-83EB-03E37E17E2B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6BD03130-4931-410F-83EB-03E37E17E2B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6BD03130-4931-410F-83EB-03E37E17E2B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6BD03130-4931-410F-83EB-03E37E17E2B9}.Release|Any CPU.Build.0 = Release|Any CPU
- {3DE6A4B2-9241-4605-9E8C-BD17A1F71502}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3DE6A4B2-9241-4605-9E8C-BD17A1F71502}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3DE6A4B2-9241-4605-9E8C-BD17A1F71502}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3DE6A4B2-9241-4605-9E8C-BD17A1F71502}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3DA6F9E0-BE9F-4F16-8A41-A68948A01467}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3DA6F9E0-BE9F-4F16-8A41-A68948A01467}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3DA6F9E0-BE9F-4F16-8A41-A68948A01467}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3DA6F9E0-BE9F-4F16-8A41-A68948A01467}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F8A8F3D9-4C7E-442A-8ADF-F0C8586F41B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F8A8F3D9-4C7E-442A-8ADF-F0C8586F41B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F8A8F3D9-4C7E-442A-8ADF-F0C8586F41B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F8A8F3D9-4C7E-442A-8ADF-F0C8586F41B7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CB8CB4AF-B10B-4F0D-813F-5F9DA23B49FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CB8CB4AF-B10B-4F0D-813F-5F9DA23B49FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CB8CB4AF-B10B-4F0D-813F-5F9DA23B49FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CB8CB4AF-B10B-4F0D-813F-5F9DA23B49FA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {73DCD314-D2DE-452D-8B7A-9DAA964F0DCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {73DCD314-D2DE-452D-8B7A-9DAA964F0DCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {73DCD314-D2DE-452D-8B7A-9DAA964F0DCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {73DCD314-D2DE-452D-8B7A-9DAA964F0DCD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
- {2DF1C042-7544-4885-AFE4-E7010369A519} = {5613E456-DD6D-4A16-AB48-D2F51CAC0526}
- {ACB52D46-6553-44EF-89AE-D3038406FA1E} = {5613E456-DD6D-4A16-AB48-D2F51CAC0526}
- {6BD03130-4931-410F-83EB-03E37E17E2B9} = {5613E456-DD6D-4A16-AB48-D2F51CAC0526}
- {3DE6A4B2-9241-4605-9E8C-BD17A1F71502} = {5613E456-DD6D-4A16-AB48-D2F51CAC0526}
+ {3DA6F9E0-BE9F-4F16-8A41-A68948A01467} = {1F423A0A-64D9-400C-B2B0-3AC378A5AB02}
+ {F8A8F3D9-4C7E-442A-8ADF-F0C8586F41B7} = {1F423A0A-64D9-400C-B2B0-3AC378A5AB02}
+ {CB8CB4AF-B10B-4F0D-813F-5F9DA23B49FA} = {1F423A0A-64D9-400C-B2B0-3AC378A5AB02}
+ {73DCD314-D2DE-452D-8B7A-9DAA964F0DCD} = {1F423A0A-64D9-400C-B2B0-3AC378A5AB02}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7569C2D0-1323-45B5-8CFF-3ECACD9E0B82}
diff --git a/solutions/Virtual-Assistant/src/csharp/assistant/BotServices.cs b/solutions/Virtual-Assistant/src/csharp/assistant/BotServices.cs
index ff1b4eac95..a35de8bcd4 100644
--- a/solutions/Virtual-Assistant/src/csharp/assistant/BotServices.cs
+++ b/solutions/Virtual-Assistant/src/csharp/assistant/BotServices.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Bot.Builder.AI.Luis;
@@ -27,7 +28,7 @@ public class BotServices
/// Initializes a new instance of the class.
///
/// The instance for the bot.
- public BotServices(BotConfiguration botConfiguration)
+ public BotServices(BotConfiguration botConfiguration, List skills)
{
foreach (var service in botConfiguration.Services)
{
@@ -102,6 +103,25 @@ public BotServices(BotConfiguration botConfiguration)
}
}
}
+
+ foreach(var skill in skills)
+ {
+ var skillConfig = new SkillConfiguration()
+ {
+ AuthConnectionName = AuthConnectionName,
+ CosmosDbOptions = CosmosDbOptions,
+ TelemetryClient = TelemetryClient,
+ LuisServices = LuisServices.Where(l => skill.LuisServiceIds.Contains(l.Key) == true).ToDictionary(l => l.Key, l => l.Value as LuisRecognizer),
+ };
+
+ foreach (var set in skill.Configuration)
+ {
+ skillConfig.Properties.Add(set.Key, set.Value);
+ }
+
+ SkillDefinitions.Add(skill);
+ SkillConfigurations.Add(skill.Id, skillConfig);
+ }
}
public CosmosDbStorageOptions CosmosDbOptions { get; }
@@ -157,6 +177,9 @@ public BotServices(BotConfiguration botConfiguration)
///
public Dictionary QnAServices { get; } = new Dictionary();
- public List RegisteredSkills { get; set; } = new List();
+ public List SkillDefinitions { get; set; } = new List();
+
+ public Dictionary SkillConfigurations = new Dictionary();
+
}
}
diff --git a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Cancel/CancelResponses.cs b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Cancel/CancelResponses.cs
index c024d2aa59..be48527db0 100644
--- a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Cancel/CancelResponses.cs
+++ b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Cancel/CancelResponses.cs
@@ -3,6 +3,8 @@
using VirtualAssistant.Dialogs.Cancel.Resources;
using Microsoft.Bot.Builder.TemplateManager;
+using Microsoft.Bot.Builder;
+using Microsoft.Bot.Schema;
namespace VirtualAssistant
{
@@ -19,8 +21,8 @@ public class CancelResponses : TemplateManager
["default"] = new TemplateIdMap
{
{ _confirmPrompt, (context, data) => CancelStrings.CANCEL_PROMPT },
- { _cancelConfirmed, (context, data) => CancelStrings.CANCEL_CONFIRMED },
- { _cancelDenied, (context, data) => CancelStrings.CANCEL_DENIED },
+ { _cancelConfirmed, (context, data) => SendAcceptingInputReply(context, CancelStrings.CANCEL_CONFIRMED) },
+ { _cancelDenied, (context, data) => SendAcceptingInputReply(context, CancelStrings.CANCEL_DENIED) },
},
["en"] = new TemplateIdMap { },
["fr"] = new TemplateIdMap { },
@@ -30,5 +32,14 @@ public CancelResponses()
{
Register(new DictionaryRenderer(_responseTemplates));
}
+
+ public static IMessageActivity SendAcceptingInputReply(ITurnContext turnContext, string text)
+ {
+ var reply = turnContext.Activity.CreateReply();
+ reply.InputHint = InputHints.AcceptingInput;
+ reply.Text = text;
+
+ return reply;
+ }
}
}
diff --git a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Escalate/EscalateResponses.cs b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Escalate/EscalateResponses.cs
index ac15cce85e..e17087ac17 100644
--- a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Escalate/EscalateResponses.cs
+++ b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Escalate/EscalateResponses.cs
@@ -3,6 +3,8 @@
using VirtualAssistant.Dialogs.Escalate.Resources;
using Microsoft.Bot.Builder.TemplateManager;
+using Microsoft.Bot.Schema;
+using Microsoft.Bot.Builder;
namespace VirtualAssistant
{
@@ -14,7 +16,7 @@ public class EscalateResponses : TemplateManager
{
["default"] = new TemplateIdMap
{
- { SendPhone, (context, data) => EscalateStrings.PHONE_INFO },
+ { SendPhone, (context, data) => SendAcceptingInputReply(context, EscalateStrings.PHONE_INFO) },
},
["en"] = new TemplateIdMap { },
["fr"] = new TemplateIdMap { },
@@ -24,5 +26,14 @@ public EscalateResponses()
{
Register(new DictionaryRenderer(_responseTemplates));
}
+
+ public static IMessageActivity SendAcceptingInputReply(ITurnContext turnContext, string text)
+ {
+ var reply = turnContext.Activity.CreateReply();
+ reply.InputHint = InputHints.AcceptingInput;
+ reply.Text = text;
+
+ return reply;
+ }
}
}
diff --git a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Main/MainDialog.cs b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Main/MainDialog.cs
index bb2bf97831..f0919c5f9e 100644
--- a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Main/MainDialog.cs
+++ b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Main/MainDialog.cs
@@ -18,20 +18,15 @@ namespace VirtualAssistant
{
public class MainDialog : RouterDialog
{
- // Constants
- public const string Name = "MainDialog";
- private const string LocationEvent = "IPA.Location";
- private const string TimezoneEvent = "IPA.Timezone";
-
// Fields
private BotServices _services;
private BotConfiguration _botConfig;
private UserState _userState;
private ConversationState _conversationState;
- private SkillRouter _skillRouter;
private IStatePropertyAccessor _onboardingState;
private IStatePropertyAccessor> _parametersAccessor;
private MainResponses _responder = new MainResponses();
+ private SkillRouter _skillRouter;
public MainDialog(BotServices services, BotConfiguration botConfig, ConversationState conversationState, UserState userState)
: base(nameof(MainDialog))
@@ -42,13 +37,14 @@ public MainDialog(BotServices services, BotConfiguration botConfig, Conversation
_userState = userState;
_onboardingState = _userState.CreateProperty(nameof(OnboardingState));
_parametersAccessor = _userState.CreateProperty>("userInfo");
+ var dialogState = _conversationState.CreateProperty(nameof(DialogState));
AddDialog(new OnboardingDialog(_services, _onboardingState));
AddDialog(new EscalateDialog(_services));
- AddDialog(new CustomSkillDialog(_services));
+ AddDialog(new CustomSkillDialog(_services.SkillConfigurations, dialogState));
// Initialize skill dispatcher
- _skillRouter = new SkillRouter(_services.RegisteredSkills);
+ _skillRouter = new SkillRouter(_services.SkillDefinitions);
}
protected override async Task OnStartAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
@@ -82,7 +78,7 @@ public MainDialog(BotServices services, BotConfiguration botConfig, Conversation
var luisService = _services.LuisServices["general"];
var luisResult = await luisService.RecognizeAsync(dc.Context, CancellationToken.None);
var luisIntent = luisResult?.TopIntent().intent;
-
+
// switch on general intents
switch (luisIntent)
{
@@ -137,9 +133,7 @@ public MainDialog(BotServices services, BotConfiguration botConfig, Conversation
await RouteToSkillAsync(dc, new SkillDialogOptions()
{
- LuisResult = luisResult,
- MatchedSkill = matchedSkill,
- LuisService = _botConfig.FindServiceByNameOrId(matchedSkill.LuisServiceId) as LuisService,
+ SkillDefinition = matchedSkill,
Parameters = parameters,
});
@@ -154,9 +148,7 @@ public MainDialog(BotServices services, BotConfiguration botConfig, Conversation
await RouteToSkillAsync(dc, new SkillDialogOptions()
{
- LuisResult = luisResult,
- MatchedSkill = matchedSkill,
- LuisService = _botConfig.FindServiceByNameOrId(matchedSkill.LuisServiceId) as LuisService,
+ SkillDefinition = matchedSkill,
Parameters = parameters,
});
@@ -171,9 +163,7 @@ public MainDialog(BotServices services, BotConfiguration botConfig, Conversation
await RouteToSkillAsync(dc, new SkillDialogOptions()
{
- LuisResult = luisResult,
- MatchedSkill = matchedSkill,
- LuisService = _botConfig.FindServiceByNameOrId(matchedSkill.LuisServiceId) as LuisService,
+ SkillDefinition = matchedSkill,
Parameters = parameters,
});
@@ -188,9 +178,7 @@ public MainDialog(BotServices services, BotConfiguration botConfig, Conversation
await RouteToSkillAsync(dc, new SkillDialogOptions()
{
- LuisResult = luisResult,
- MatchedSkill = matchedSkill,
- LuisService = _botConfig.FindServiceByNameOrId(matchedSkill.LuisServiceId) as LuisService,
+ SkillDefinition = matchedSkill,
Parameters = parameters,
});
@@ -214,10 +202,10 @@ public MainDialog(BotServices services, BotConfiguration botConfig, Conversation
private async Task RouteToSkillAsync(DialogContext dc, SkillDialogOptions options)
{
// If we can't handle this within the local Bot it's a skill (prefix of s will make this clearer)
- if (options.MatchedSkill != null)
+ if (options.SkillDefinition != null)
{
// We have matched to a Skill
- await dc.Context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"-->Forwarding your utterance to the {options.MatchedSkill.Name} skill."));
+ await dc.Context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"-->Forwarding your utterance to the {options.SkillDefinition.Name} skill."));
// Begin the SkillDialog and pass the arguments in
await dc.BeginDialogAsync(nameof(CustomSkillDialog), options);
@@ -225,7 +213,7 @@ private async Task RouteToSkillAsync(DialogContext dc, SkillDialogOptions option
// Pass the activity we have
var result = await dc.ContinueDialogAsync();
- if(result.Status == DialogTurnStatus.Complete)
+ if (result.Status == DialogTurnStatus.Complete)
{
await CompleteAsync(dc);
}
@@ -242,14 +230,18 @@ private async Task RouteToSkillAsync(DialogContext dc, SkillDialogOptions option
protected override async Task OnEventAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
{
+ // Indicates whether the event activity should be sent to the active dialog on the stack
+ var forward = true;
var ev = dc.Context.Activity.AsEventActivity();
- await dc.Context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"Received event: {ev.Name}"));
-
var parameters = await _parametersAccessor.GetAsync(dc.Context, () => new Dictionary());
+ // Send trace to emulator
+ var trace = new Activity(type: ActivityTypes.Trace, text: $"Received event: {ev.Name}");
+ await dc.Context.SendActivityAsync(trace);
+
switch (ev.Name)
{
- case TimezoneEvent:
+ case Events.TimezoneEvent:
{
try
{
@@ -263,57 +255,56 @@ private async Task RouteToSkillAsync(DialogContext dc, SkillDialogOptions option
await dc.Context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"Timezone passed could not be mapped to a valid Timezone. Property not set."));
}
+ forward = false;
break;
}
- case LocationEvent:
+ case Events.LocationEvent:
{
parameters[ev.Name] = ev.Value;
+ forward = false;
break;
}
- case "tokens/response":
+ case Events.TokenResponseEvent:
{
- // Auth dialog completion
- var result = await dc.ContinueDialogAsync();
-
- if (result.Status == DialogTurnStatus.Complete)
- {
- await CompleteAsync(dc);
- }
-
+ forward = true;
break;
}
- case "POI.ActiveLocation":
- case "POI.ActiveRoute":
+ case Events.ActiveLocationUpdate:
+ case Events.ActiveRouteUpdate:
{
var matchedSkill = _skillRouter.IdentifyRegisteredSkill(Dispatch.Intent.l_PointOfInterest.ToString());
await RouteToSkillAsync(dc, new SkillDialogOptions()
{
- MatchedSkill = matchedSkill,
- LuisService = _botConfig.FindServiceByNameOrId(matchedSkill.LuisServiceId) as LuisService,
- Parameters = parameters,
+ SkillDefinition = matchedSkill
});
+ forward = false;
break;
}
+ }
- default:
- {
- if (dc.ActiveDialog != null)
- {
- var result = await dc.ContinueDialogAsync();
+ if (forward)
+ {
+ var result = await dc.ContinueDialogAsync();
- if (result.Status == DialogTurnStatus.Complete)
- {
- await CompleteAsync(dc);
- }
- }
- break;
- }
+ if (result.Status == DialogTurnStatus.Complete)
+ {
+ await CompleteAsync(dc);
+ }
}
}
}
+
+ public static class Events
+ {
+ public const string TokenResponseEvent = "tokens/response";
+ public const string TimezoneEvent = "IPA.Timezone";
+ public const string LocationEvent = "IPA.Location";
+ public const string ActiveLocationUpdate = "POI.ActiveLocation";
+ public const string ActiveRouteUpdate = "POI.ActiveRoute";
+ }
}
diff --git a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Main/MainResponses.cs b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Main/MainResponses.cs
index 4b7e3d7f48..a15b512887 100644
--- a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Main/MainResponses.cs
+++ b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Main/MainResponses.cs
@@ -25,10 +25,10 @@ public class MainResponses : TemplateManager
{
["default"] = new TemplateIdMap
{
- { Cancelled, (context, data) => MainStrings.CANCELLED },
- { Completed, (context, data) => MainStrings.COMPLETED },
- { Confused, (context, data) => MainStrings.CONFUSED },
- { Greeting, (context, data) => MainStrings.GREETING },
+ { Cancelled, (context, data) => SendAcceptingInputReply(context, MainStrings.CANCELLED) },
+ { Completed, (context, data) => SendAcceptingInputReply(context, MainStrings.COMPLETED) },
+ { Confused, (context, data) => SendAcceptingInputReply(context, MainStrings.CONFUSED) },
+ { Greeting, (context, data) => SendAcceptingInputReply(context, MainStrings.GREETING) },
{ Help, (context, data) => SendHelpCard(context, data) },
{ Intro, (context, data) => SendIntroCard(context, data) },
},
@@ -83,5 +83,14 @@ public static IMessageActivity SendHelpCard(ITurnContext turnContext, dynamic da
};
return response;
}
+
+ public static IMessageActivity SendAcceptingInputReply(ITurnContext turnContext, string text)
+ {
+ var reply = turnContext.Activity.CreateReply();
+ reply.InputHint = InputHints.AcceptingInput;
+ reply.Text = text;
+
+ return reply;
+ }
}
}
diff --git a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Onboarding/OnboardingDialog.cs b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Onboarding/OnboardingDialog.cs
index 2bd869fa1a..4aa4fc745c 100644
--- a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Onboarding/OnboardingDialog.cs
+++ b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Onboarding/OnboardingDialog.cs
@@ -47,7 +47,7 @@ public async Task AskForName(WaterfallStepContext sc, Cancella
public async Task AskForLocation(WaterfallStepContext sc, CancellationToken cancellationToken)
{
- _state = await _accessor.GetAsync(sc.Context);
+ _state = await _accessor.GetAsync(sc.Context, () => new OnboardingState());
_state.Name = (string)sc.Result;
return await sc.PromptAsync(LocationPrompt, new PromptOptions()
diff --git a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Shared/CustomSkillDialog.cs b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Shared/CustomSkillDialog.cs
index facfcdaf46..ceb05eb7cc 100644
--- a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Shared/CustomSkillDialog.cs
+++ b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Shared/CustomSkillDialog.cs
@@ -1,4 +1,5 @@
-using Microsoft.Bot.Builder.Dialogs;
+using Microsoft.Bot.Builder;
+using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Solutions.Skills;
using System;
using System.Collections.Generic;
@@ -10,10 +11,10 @@ namespace VirtualAssistant
{
public class CustomSkillDialog : ComponentDialog
{
- public CustomSkillDialog(BotServices botServices)
+ public CustomSkillDialog(Dictionary skills, IStatePropertyAccessor accessor)
: base(nameof(CustomSkillDialog))
{
- AddDialog(new SkillDialog(botServices.CosmosDbOptions, botServices.TelemetryClient));
+ AddDialog(new SkillDialog(skills, accessor));
}
protected override Task EndComponentAsync(DialogContext outerDc, object result, CancellationToken cancellationToken)
diff --git a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Shared/Resources/Dispatch.cs b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Shared/Resources/Dispatch.cs
index f4c0cc3c75..88bb95148c 100644
--- a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Shared/Resources/Dispatch.cs
+++ b/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Shared/Resources/Dispatch.cs
@@ -19,6 +19,7 @@ public enum Intent
l_General,
l_ToDo,
l_PointOfInterest,
+ l_News,
q_FAQ,
None
};
diff --git a/solutions/Virtual-Assistant/src/csharp/assistant/Startup.cs b/solutions/Virtual-Assistant/src/csharp/assistant/Startup.cs
index 23f9475d4e..956c53085f 100644
--- a/solutions/Virtual-Assistant/src/csharp/assistant/Startup.cs
+++ b/solutions/Virtual-Assistant/src/csharp/assistant/Startup.cs
@@ -45,8 +45,8 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton(sp => botConfig ?? throw new InvalidOperationException($"The .bot config file could not be loaded."));
// Initializes your bot service clients and adds a singleton that your Bot can access through dependency injection.
- var connectedServices = new BotServices(botConfig);
- connectedServices.RegisteredSkills = Configuration.GetSection("skills").Get>();
+ var skills = Configuration.GetSection("skills").Get>();
+ var connectedServices = new BotServices(botConfig, skills);
services.AddSingleton(sp => connectedServices);
// Initialize Bot State
@@ -91,7 +91,7 @@ public void ConfigureServices(IServiceCollection services)
options.OnTurnError = async (context, exception) =>
{
await context.SendActivityAsync("Sorry, it looks like something went wrong. Please try again.");
- await context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"Skill Error: {exception.Message} | {exception.StackTrace}"));
+ await context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"Virtual Assistant Error: {exception.Message} | {exception.StackTrace}"));
connectedServices.TelemetryClient.TrackException(exception);
};
diff --git a/solutions/Virtual-Assistant/src/csharp/assistant/VirtualAssistant.csproj b/solutions/Virtual-Assistant/src/csharp/assistant/VirtualAssistant.csproj
index fe59f20546..80fd0fc221 100644
--- a/solutions/Virtual-Assistant/src/csharp/assistant/VirtualAssistant.csproj
+++ b/solutions/Virtual-Assistant/src/csharp/assistant/VirtualAssistant.csproj
@@ -89,32 +89,12 @@
+
+
+
+
-
-
- ..\skills\bin\Debug\netcoreapp2.1\CalendarSkill.dll
-
-
- ..\skills\bin\Debug\netcoreapp2.1\DemoSkill.dll
-
-
- ..\skills\bin\Debug\netcoreapp2.1\EmailSkill.dll
-
-
- ..\skills\bin\Debug\netcoreapp2.1\PointOfInterestSkill.dll
-
-
- ..\skills\bin\Debug\netcoreapp2.1\ToDoSkill.dll
-
-
-
-
-
-
-
-
-
Always
diff --git a/solutions/Virtual-Assistant/src/csharp/assistant/appsettings.json b/solutions/Virtual-Assistant/src/csharp/assistant/appsettings.json
index f27408dfe9..dd696bbbcf 100644
--- a/solutions/Virtual-Assistant/src/csharp/assistant/appsettings.json
+++ b/solutions/Virtual-Assistant/src/csharp/assistant/appsettings.json
@@ -12,7 +12,10 @@
"assembly": "CalendarSkill.CalendarSkill, CalendarSkill, Version=1.0.0.0, Culture=neutral",
"dispatchIntent": "l_Calendar",
"authConnectionName": "",
- "luisServiceId": "calendar",
+ "luisServiceIds": [
+ "calendar",
+ "general"
+ ],
"parameters": [
"IPA.Timezone"
]
@@ -24,7 +27,10 @@
"assembly": "EmailSkill.EmailSkill, EmailSkill, Version=1.0.0.0, Culture=neutral",
"dispatchIntent": "l_Email",
"authConnectionName": "",
- "luisServiceId": "email",
+ "luisServiceIds": [
+ "email",
+ "general"
+ ],
"parameters": [
"IPA.Timezone"
]
@@ -36,7 +42,10 @@
"assembly": "ToDoSkill.ToDoSkill, ToDoSkill, Version=1.0.0.0, Culture=neutral",
"dispatchIntent": "l_ToDo",
"authConnectionName": "",
- "luisServiceId": "todo"
+ "luisServiceIds": [
+ "todo",
+ "general"
+ ]
},
{
"type": "skill",
@@ -45,7 +54,10 @@
"assembly": "PointOfInterestSkill.PointOfInterestSkill, PointOfInterestSkill, Version=1.0.0.0, Culture=neutral",
"dispatchIntent": "l_PointOfInterest",
"authConnectionName": "",
- "luisServiceId": "pointofinterest",
+ "luisServiceIds": [
+ "pointofinterest",
+ "general"
+ ],
"parameters": [
"IPA.Location",
"IPA.Timezone"
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Dialogs/InterruptionStatus.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Dialogs/InterruptionAction.cs
similarity index 100%
rename from solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Dialogs/InterruptionStatus.cs
rename to solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Dialogs/InterruptionAction.cs
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Dialogs/RouterDialog.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Dialogs/RouterDialog.cs
index 6f59cc9762..2b393e41cf 100644
--- a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Dialogs/RouterDialog.cs
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Dialogs/RouterDialog.cs
@@ -50,6 +50,7 @@ public RouterDialog(string dialogId)
}
case DialogTurnStatus.Complete:
+ case DialogTurnStatus.Cancelled:
{
await CompleteAsync(innerDc, result);
break;
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Microsoft.Bot.Solutions.csproj b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Microsoft.Bot.Solutions.csproj
index 8c687b0e69..acd3d3738c 100644
--- a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Microsoft.Bot.Solutions.csproj
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Microsoft.Bot.Solutions.csproj
@@ -36,8 +36,6 @@
Always
- TextTemplatingFileGenerator
- OnboardInfoCard.cs
Always
@@ -53,4 +51,6 @@
+
+
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/LuisTelemetryConstants.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/LuisTelemetryConstants.cs
new file mode 100644
index 0000000000..25eb776c1f
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/LuisTelemetryConstants.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Bot.Solutions
+{
+ ///
+ /// The Application Insights property names that we're logging.
+ ///
+ public static class LuisTelemetryConstants
+ {
+ public const string IntentPrefix = "LuisIntent"; // Application Insights Custom Event name (with Intent)
+ public const string IntentProperty = "Intent";
+ public const string IntentScoreProperty = "IntentScore";
+ public const string ConversationIdProperty = "ConversationId";
+ public const string QuestionProperty = "Question";
+ public const string ActivityIdProperty = "ActivityId";
+ public const string SentimentLabelProperty = "SentimentLabel";
+ public const string SentimentScoreProperty = "SentimentScore";
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/QnATelemetryConstants.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/QnATelemetryConstants.cs
new file mode 100644
index 0000000000..3740feab54
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/QnATelemetryConstants.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Bot.Solutions
+{
+ ///
+ /// The Application Insights property names that we're logging.
+ ///
+ public static class QnATelemetryConstants
+ {
+ public const string ActivityIdProperty = "ActivityId";
+ public const string UsernameProperty = "Username";
+ public const string ConversationIdProperty = "ConversationId";
+ public const string OriginalQuestionProperty = "OriginalQuestion";
+ public const string QuestionProperty = "Question";
+ public const string AnswerProperty = "Answer";
+ public const string ScoreProperty = "Score";
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/TelemetryConstants.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/TelemetryConstants.cs
new file mode 100644
index 0000000000..3bd73eb79f
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/TelemetryConstants.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Bot.Solutions
+{
+ public static class TelemetryConstants
+ {
+ public const string ActivityIDProperty = "ActivityId";
+ public const string ChannelProperty = "Channel";
+ public const string FromIdProperty = "FromId";
+ public const string FromNameProperty = "FromName";
+ public const string RecipientIdProperty = "RecipientId";
+ public const string RecipientNameProperty = "RecipientName";
+ public const string ConversationIdProperty = "ConversationId";
+ public const string ConversationNameProperty = "ConversationName";
+ public const string TextProperty = "Text";
+ public const string LocaleProperty = "Locale";
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/TelemetryLoggerMiddleware.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/TelemetryLoggerMiddleware.cs
new file mode 100644
index 0000000000..52ab945e26
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/TelemetryLoggerMiddleware.cs
@@ -0,0 +1,274 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.ApplicationInsights;
+using Microsoft.ApplicationInsights.Extensibility;
+using Microsoft.Bot.Builder;
+using Microsoft.Bot.Schema;
+
+namespace Microsoft.Bot.Solutions
+{
+ ///
+ /// Middleware for logging incoming, outgoing, updated or deleted Activity messages into Application Insights.
+ /// In addition, registers the telemetry client in the context so other Application Insights components can log
+ /// telemetry.
+ /// If this Middleware is removed, all the other sample components don't log (but still operate).
+ ///
+ public class TelemetryLoggerMiddleware : IMiddleware
+ {
+ public static readonly string AppInsightsServiceKey = $"{nameof(TelemetryLoggerMiddleware)}.AppInsightsContext";
+
+ // Application Insights Custom Event name, logged when new message is received from the user
+ public static readonly string BotMsgReceiveEvent = "BotMessageReceived";
+
+ // Application Insights Custom Event name, logged when a message is sent out from the bot
+ public static readonly string BotMsgSendEvent = "BotMessageSend";
+
+ // Application Insights Custom Event name, logged when a message is updated by the bot (rare case)
+ public static readonly string BotMsgUpdateEvent = "BotMessageUpdate";
+
+ // Application Insights Custom Event name, logged when a message is deleted by the bot (rare case)
+ public static readonly string BotMsgDeleteEvent = "BotMessageDelete";
+
+ private TelemetryClient _telemetryClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Application Insights instrumentation key. See Application Insights for more information.
+ /// (Optional) Enable/Disable logging user name within Application Insights.
+ /// (Optional) Enable/Disable logging original message name within Application Insights.
+ /// (Optional) TelemetryConfiguration to use for Application Insights.
+ public TelemetryLoggerMiddleware(string instrumentationKey, bool logUserName = false, bool logOriginalMessage = false, TelemetryConfiguration config = null)
+ {
+ if (string.IsNullOrWhiteSpace(instrumentationKey))
+ {
+ throw new ArgumentNullException(nameof(instrumentationKey));
+ }
+
+ var telemetryConfiguration = config ?? new TelemetryConfiguration(instrumentationKey);
+ this._telemetryClient = new TelemetryClient(telemetryConfiguration);
+ this.LogUserName = logUserName;
+ this.LogOriginalMessage = logOriginalMessage;
+ }
+
+ ///
+ /// Gets a value indicating whether indicates whether to log the user name into the BotMessageReceived event.
+ ///
+ ///
+ /// A value indicating whether indicates whether to log the user name into the BotMessageReceived event.
+ ///
+ public bool LogUserName { get; }
+
+ ///
+ /// Gets a value indicating whether indicates whether to log the original message into the BotMessageReceived event.
+ ///
+ ///
+ /// Indicates whether to log the original message into the BotMessageReceived event.
+ ///
+ public bool LogOriginalMessage { get; }
+
+ ///
+ /// Records incoming and outgoing activities to the Application Insights store.
+ ///
+ /// The context object for this turn.
+ /// The delegate to call to continue the bot middleware pipeline.
+ /// A cancellation token that can be used by other objects
+ /// or threads to receive notice of cancellation.
+ /// A task that represents the work queued to execute.
+ ///
+ ///
+ public async Task OnTurnAsync(ITurnContext context, NextDelegate nextTurn, CancellationToken cancellationToken)
+ {
+ BotAssert.ContextNotNull(context);
+
+ context.TurnState.Add(TelemetryLoggerMiddleware.AppInsightsServiceKey, this._telemetryClient);
+
+ // log incoming activity at beginning of turn
+ if (context.Activity != null)
+ {
+ var activity = context.Activity;
+
+ // Context properties for App Insights
+ if (!string.IsNullOrEmpty(activity.Conversation.Id))
+ {
+ this._telemetryClient.Context.Session.Id = activity.Conversation.Id;
+ }
+
+ if (!string.IsNullOrEmpty(activity.From.Id))
+ {
+ this._telemetryClient.Context.User.Id = activity.From.Id;
+ }
+
+ // Log the Application Insights Bot Message Received
+ this._telemetryClient.TrackEvent(BotMsgReceiveEvent, this.FillReceiveEventProperties(activity));
+ }
+
+ // hook up onSend pipeline
+ context.OnSendActivities(async (ctx, activities, nextSend) =>
+ {
+ // run full pipeline
+ var responses = await nextSend().ConfigureAwait(false);
+
+ foreach (var activity in activities)
+ {
+ this._telemetryClient.TrackEvent(BotMsgSendEvent, this.FillSendEventProperties(activity));
+ }
+
+ return responses;
+ });
+
+ // hook up update activity pipeline
+ context.OnUpdateActivity(async (ctx, activity, nextUpdate) =>
+ {
+ // run full pipeline
+ var response = await nextUpdate().ConfigureAwait(false);
+
+ this._telemetryClient.TrackEvent(BotMsgUpdateEvent, this.FillUpdateEventProperties(activity));
+
+ return response;
+ });
+
+ // hook up delete activity pipeline
+ context.OnDeleteActivity(async (ctx, reference, nextDelete) =>
+ {
+ // run full pipeline
+ await nextDelete().ConfigureAwait(false);
+
+ var deleteActivity = new Activity
+ {
+ Type = ActivityTypes.MessageDelete,
+ Id = reference.ActivityId,
+ }
+ .ApplyConversationReference(reference, isIncoming: false)
+ .AsMessageDeleteActivity();
+
+ this._telemetryClient.TrackEvent(BotMsgDeleteEvent, this.FillDeleteEventProperties(deleteActivity));
+ });
+
+ if (nextTurn != null)
+ {
+ await nextTurn(cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Fills the Application Insights Custom Event properties for BotMessageReceived.
+ /// These properties are logged in the custom event when a new message is received from the user.
+ ///
+ /// Last activity sent from user.
+ /// A dictionary that is sent as "Properties" to Application Insights TrackEvent method for the BotMessageReceived Message.
+ private Dictionary FillReceiveEventProperties(Activity activity)
+ {
+ var properties = new Dictionary()
+ {
+ { TelemetryConstants.ActivityIDProperty, activity.Id },
+ { TelemetryConstants.ChannelProperty, activity.ChannelId },
+ { TelemetryConstants.FromIdProperty, activity.From.Id },
+ { TelemetryConstants.ConversationIdProperty, activity.Conversation.Id },
+ { TelemetryConstants.ConversationNameProperty, activity.Conversation.Name },
+ { TelemetryConstants.LocaleProperty, activity.Locale },
+ };
+
+ // For some customers, logging user name within Application Insights might be an issue so have provided a config setting to disable this feature
+ if (this.LogUserName && !string.IsNullOrWhiteSpace(activity.From.Name))
+ {
+ properties.Add(TelemetryConstants.FromNameProperty, activity.From.Name);
+ }
+
+ // For some customers, logging the utterances within Application Insights might be an so have provided a config setting to disable this feature
+ if (this.LogOriginalMessage && !string.IsNullOrWhiteSpace(activity.Text))
+ {
+ properties.Add(TelemetryConstants.TextProperty, activity.Text);
+ }
+
+ return properties;
+ }
+
+ ///
+ /// Fills the Application Insights Custom Event properties for BotMessageSend.
+ /// These properties are logged in the custom event when a response message is sent by the Bot to the user.
+ ///
+ /// Last activity sent from user.
+ /// A dictionary that is sent as "Properties" to Application Insights TrackEvent method for the BotMessageSend Message.
+ private Dictionary FillSendEventProperties(Activity activity)
+ {
+ var properties = new Dictionary()
+ {
+ { TelemetryConstants.ActivityIDProperty, activity.Id },
+ { TelemetryConstants.ChannelProperty, activity.ChannelId },
+ { TelemetryConstants.RecipientIdProperty, activity.Recipient.Id },
+ { TelemetryConstants.ConversationIdProperty, activity.Conversation.Id },
+ { TelemetryConstants.ConversationNameProperty, activity.Conversation.Name },
+ { TelemetryConstants.LocaleProperty, activity.Locale },
+ };
+
+ // For some customers, logging user name within Application Insights might be an issue so have provided a config setting to disable this feature
+ if (this.LogUserName && !string.IsNullOrWhiteSpace(activity.Recipient.Name))
+ {
+ properties.Add(TelemetryConstants.RecipientNameProperty, activity.Recipient.Name);
+ }
+
+ // For some customers, logging the utterances within Application Insights might be an so have provided a config setting to disable this feature
+ if (this.LogOriginalMessage && !string.IsNullOrWhiteSpace(activity.Text))
+ {
+ properties.Add(TelemetryConstants.TextProperty, activity.Text);
+ }
+
+ return properties;
+ }
+
+ ///
+ /// Fills the Application Insights Custom Event properties for BotMessageUpdate.
+ /// These properties are logged in the custom event when an activity message is updated by the Bot.
+ /// For example, if a card is interacted with by the use, and the card needs to be updated to reflect
+ /// some interaction.
+ ///
+ /// Last activity sent from user.
+ /// A dictionary that is sent as "Properties" to Application Insights TrackEvent method for the BotMessageUpdate Message.
+ private Dictionary FillUpdateEventProperties(Activity activity)
+ {
+ var properties = new Dictionary()
+ {
+ { TelemetryConstants.ActivityIDProperty, activity.Id },
+ { TelemetryConstants.ChannelProperty, activity.ChannelId },
+ { TelemetryConstants.RecipientIdProperty, activity.Recipient.Id },
+ { TelemetryConstants.ConversationIdProperty, activity.Conversation.Id },
+ { TelemetryConstants.ConversationNameProperty, activity.Conversation.Name },
+ { TelemetryConstants.LocaleProperty, activity.Locale },
+ };
+
+ // For some customers, logging the utterances within Application Insights might be an so have provided a config setting to disable this feature
+ if (this.LogOriginalMessage && !string.IsNullOrWhiteSpace(activity.Text))
+ {
+ properties.Add(TelemetryConstants.TextProperty, activity.Text);
+ }
+
+ return properties;
+ }
+
+ ///
+ /// Fills the Application Insights Custom Event properties for BotMessageDelete.
+ /// These properties are logged in the custom event when an activity message is deleted by the Bot. This is a relatively rare case.
+ ///
+ /// Last activity sent from user.
+ /// A dictionary that is sent as "Properties" to Application Insights TrackEvent method for the BotMessageDelete Message.
+ private Dictionary FillDeleteEventProperties(IMessageDeleteActivity activity)
+ {
+ var properties = new Dictionary()
+ {
+ { TelemetryConstants.ActivityIDProperty, activity.Id },
+ { TelemetryConstants.ChannelProperty, activity.ChannelId },
+ { TelemetryConstants.RecipientIdProperty, activity.Recipient.Id },
+ { TelemetryConstants.ConversationIdProperty, activity.Conversation.Id },
+ { TelemetryConstants.ConversationNameProperty, activity.Conversation.Name },
+ };
+
+ return properties;
+ }
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/TelemetryLuisRecognizer.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/TelemetryLuisRecognizer.cs
new file mode 100644
index 0000000000..04d41d168e
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/TelemetryLuisRecognizer.cs
@@ -0,0 +1,192 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.ApplicationInsights;
+using Microsoft.Bot.Builder;
+using Microsoft.Bot.Builder.AI.Luis;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.Bot.Solutions
+{
+ ///
+ /// TelemetryLuisRecognizer invokes the Luis Recognizer and logs some results into Application Insights.
+ /// Logs the Top Intent, Sentiment (label/score), (Optionally) Original Text
+ /// Along with Conversation and ActivityID.
+ /// The Custom Event name this logs is MyLuisConstants.IntentPrefix + "." + 'found intent name'
+ /// For example, if intent name was "add_calender":
+ /// LuisIntent.add_calendar
+ /// See for additional information.
+ ///
+ public class TelemetryLuisRecognizer : LuisRecognizer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The LUIS _application to use to recognize text.
+ /// The LUIS prediction options to use.
+ /// TRUE to include raw LUIS API response.
+ /// TRUE to include original user message.
+ /// TRUE to include user name.
+ public TelemetryLuisRecognizer(LuisApplication application, LuisPredictionOptions predictionOptions = null, bool includeApiResults = false, bool logOriginalMessage = false, bool logUserName = false)
+ : base(application, predictionOptions, includeApiResults)
+ {
+ LogOriginalMessage = logOriginalMessage;
+ LogUsername = logUserName;
+ }
+
+ ///
+ /// Gets a value indicating whether determines whether to log the Activity message text that came from the user.
+ ///
+ /// If true, will log the Activity Message text into the AppInsight Custome Event for Luis intents.
+ public bool LogOriginalMessage { get; }
+
+ ///
+ /// Gets a value indicating whether determines whether to log the User name.
+ ///
+ /// If true, will log the user name into the AppInsight Custom Event for Luis intents.
+ public bool LogUsername { get; }
+
+ ///
+ /// Analyze the current message text and return results of the analysis (Suggested actions and intents).
+ ///
+ /// Context object containing information for a single turn of conversation with a user.
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// Determines if the original message is logged into Application Insights. This is a privacy consideration.
+ /// The LUIS results of the analysis of the current message text in the current turn's context activity.
+ public async Task RecognizeAsync(ITurnContext context, CancellationToken ct, bool logOriginalMessage = false)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Call Luis Recognizer
+ var recognizerResult = await base.RecognizeAsync(context, ct);
+
+ var conversationId = context.Activity.Conversation.Id;
+
+ // Find the Telemetry Client
+ if (context.TurnState.TryGetValue(TelemetryLoggerMiddleware.AppInsightsServiceKey, out var telemetryClient) && recognizerResult != null)
+ {
+ var topLuisIntent = recognizerResult.GetTopScoringIntent();
+ var intentScore = topLuisIntent.score.ToString("N2");
+
+ // Add the intent score and conversation id properties
+ var telemetryProperties = new Dictionary()
+ {
+ { LuisTelemetryConstants.ActivityIdProperty, context.Activity.Id },
+ { LuisTelemetryConstants.IntentProperty, topLuisIntent.intent },
+ { LuisTelemetryConstants.IntentScoreProperty, intentScore },
+ };
+
+ if (recognizerResult.Properties.TryGetValue("sentiment", out var sentiment) && sentiment is JObject)
+ {
+ if (((JObject)sentiment).TryGetValue("label", out var label))
+ {
+ telemetryProperties.Add(LuisTelemetryConstants.SentimentLabelProperty, label.Value());
+ }
+
+ if (((JObject)sentiment).TryGetValue("score", out var score))
+ {
+ telemetryProperties.Add(LuisTelemetryConstants.SentimentScoreProperty, score.Value());
+ }
+ }
+
+ if (!string.IsNullOrEmpty(conversationId))
+ {
+ telemetryProperties.Add(LuisTelemetryConstants.ConversationIdProperty, conversationId);
+ }
+
+ // Add Luis Entitites
+ var entities = new List();
+ foreach (var entity in recognizerResult.Entities)
+ {
+ if (!entity.Key.ToString().Equals("$instance"))
+ {
+ entities.Add($"{entity.Key}: {entity.Value.First}");
+ }
+ }
+
+ // For some customers, logging user name within Application Insights might be an issue so have provided a config setting to disable this feature
+ if (logOriginalMessage && !string.IsNullOrEmpty(context.Activity.Text))
+ {
+ telemetryProperties.Add(LuisTelemetryConstants.QuestionProperty, context.Activity.Text);
+ }
+
+ // Track the event
+ ((TelemetryClient)telemetryClient).TrackEvent($"{LuisTelemetryConstants.IntentPrefix}.{topLuisIntent.intent}", telemetryProperties);
+ }
+
+ return recognizerResult;
+ }
+
+ public async Task RecognizeAsync(ITurnContext context, CancellationToken ct, bool logOriginalMessage = false)
+ where T : IRecognizerConvert, new()
+ {
+ var result = new T();
+ var recognizerResult = await base.RecognizeAsync(context, ct);
+
+ var conversationId = context.Activity.Conversation.Id;
+
+ // Find the Telemetry Client
+ if (context.TurnState.TryGetValue(TelemetryLoggerMiddleware.AppInsightsServiceKey, out var telemetryClient) && recognizerResult != null)
+ {
+ var topLuisIntent = recognizerResult.GetTopScoringIntent();
+ var intentScore = topLuisIntent.score.ToString("N2");
+
+ // Add the intent score and conversation id properties
+ var telemetryProperties = new Dictionary()
+ {
+ { LuisTelemetryConstants.ActivityIdProperty, context.Activity.Id },
+ { LuisTelemetryConstants.IntentProperty, topLuisIntent.intent },
+ { LuisTelemetryConstants.IntentScoreProperty, intentScore },
+ };
+
+ if (recognizerResult.Properties.TryGetValue("sentiment", out var sentiment) && sentiment is JObject)
+ {
+ if (((JObject)sentiment).TryGetValue("label", out var label))
+ {
+ telemetryProperties.Add(LuisTelemetryConstants.SentimentLabelProperty, label.Value());
+ }
+
+ if (((JObject)sentiment).TryGetValue("score", out var score))
+ {
+ telemetryProperties.Add(LuisTelemetryConstants.SentimentScoreProperty, score.Value());
+ }
+ }
+
+ if (!string.IsNullOrEmpty(conversationId))
+ {
+ telemetryProperties.Add(LuisTelemetryConstants.ConversationIdProperty, conversationId);
+ }
+
+ // Add Luis Entitites
+ var entities = new List();
+ foreach (var entity in recognizerResult.Entities)
+ {
+ if (!entity.Key.ToString().Equals("$instance"))
+ {
+ entities.Add($"{entity.Key}: {entity.Value.First}");
+ }
+ }
+
+ // For some customers, logging user name within Application Insights might be an issue so have provided a config setting to disable this feature
+ if (logOriginalMessage && !string.IsNullOrEmpty(context.Activity.Text))
+ {
+ telemetryProperties.Add(LuisTelemetryConstants.QuestionProperty, context.Activity.Text);
+ }
+
+ // Track the event
+ ((TelemetryClient)telemetryClient).TrackEvent($"{LuisTelemetryConstants.IntentPrefix}.{topLuisIntent.intent}", telemetryProperties);
+ }
+
+ // Convert LUIS result to LG class
+ result.Convert(recognizerResult);
+ return result;
+ }
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/TelemetryQnAMaker.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/TelemetryQnAMaker.cs
new file mode 100644
index 0000000000..b0e3f6e377
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Middleware/Telemetry/TelemetryQnAMaker.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.ApplicationInsights;
+using Microsoft.Bot.Builder;
+using Microsoft.Bot.Builder.AI.QnA;
+
+namespace Microsoft.Bot.Solutions
+{
+ ///
+ /// TelemetryQnaRecognizer invokes the Qna Maker and logs some results into Application Insights.
+ /// Logs the score, and (optionally) question
+ /// Along with Conversation and ActivityID.
+ /// The Custom Event name this logs is "QnaMessage"
+ /// See for additional information.
+ ///
+ public class TelemetryQnAMaker : QnAMaker
+ {
+ public static readonly string QnaMsgEvent = "QnaMessage";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The endpoint of the knowledge base to query.
+ /// The options for the QnA Maker knowledge base.
+ /// The flag to include username in logs.
+ /// The flag to include original message in logs.
+ /// An alternate client with which to talk to QnAMaker.
+ /// If null, a default client is used for this instance.
+ public TelemetryQnAMaker(QnAMakerEndpoint endpoint, QnAMakerOptions options = null, bool logUserName = false, bool logOriginalMessage = false, HttpClient httpClient = null)
+ : base(endpoint, options, httpClient)
+ {
+ LogUserName = logUserName;
+ LogOriginalMessage = logOriginalMessage;
+ }
+
+ public bool LogUserName { get; }
+
+ public bool LogOriginalMessage { get; }
+
+ public new async Task GetAnswersAsync(ITurnContext context)
+ {
+ // Call Qna Maker
+ var queryResults = await base.GetAnswersAsync(context);
+
+ // Find the Application Insights Telemetry Client
+ if (queryResults != null && context.TurnState.TryGetValue(TelemetryLoggerMiddleware.AppInsightsServiceKey, out var telemetryClient))
+ {
+ var telemetryProperties = new Dictionary();
+ var telemetryMetrics = new Dictionary();
+
+ // Make it so we can correlate our reports with Activity or Conversation
+ telemetryProperties.Add(QnATelemetryConstants.ActivityIdProperty, context.Activity.Id);
+ var conversationId = context.Activity.Conversation.Id;
+ if (!string.IsNullOrEmpty(conversationId))
+ {
+ telemetryProperties.Add(QnATelemetryConstants.ConversationIdProperty, conversationId);
+ }
+
+ // For some customers, logging original text name within Application Insights might be an issue
+ var text = context.Activity.Text;
+ if (LogOriginalMessage && !string.IsNullOrWhiteSpace(text))
+ {
+ telemetryProperties.Add(QnATelemetryConstants.OriginalQuestionProperty, text);
+ }
+
+ // For some customers, logging user name within Application Insights might be an issue
+ var userName = context.Activity.From.Name;
+ if (LogUserName && !string.IsNullOrWhiteSpace(userName))
+ {
+ telemetryProperties.Add(QnATelemetryConstants.UsernameProperty, userName);
+ }
+
+ // Fill in Qna Results (found or not)
+ if (queryResults.Length > 0)
+ {
+ var queryResult = queryResults[0];
+ telemetryProperties.Add(QnATelemetryConstants.QuestionProperty, string.Join(",", queryResult.Questions));
+ telemetryProperties.Add(QnATelemetryConstants.AnswerProperty, queryResult.Answer);
+ telemetryMetrics.Add(QnATelemetryConstants.ScoreProperty, queryResult.Score);
+ }
+ else
+ {
+ telemetryProperties.Add(QnATelemetryConstants.QuestionProperty, "No Qna Question matched");
+ telemetryProperties.Add(QnATelemetryConstants.AnswerProperty, "No Qna Question matched");
+ }
+
+ // Track the event
+ ((TelemetryClient)telemetryClient).TrackEvent(QnaMsgEvent, telemetryProperties, telemetryMetrics);
+ }
+
+ return queryResults;
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Resources/CommonResponses.tt b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Resources/CommonResponses.tt
index cf68d76001..aa06ac6b3c 100644
--- a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Resources/CommonResponses.tt
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Resources/CommonResponses.tt
@@ -1,4 +1,4 @@
-<#@ assembly name="$(SolutionDir)Lib\Newtonsoft.Json.dll" #>
+<#@ assembly name="Newtonsoft.Json.dll" #>
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#
diff --git a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Shared/Resources/General.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Resources/General.cs
similarity index 91%
rename from solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Shared/Resources/General.cs
rename to solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Resources/General.cs
index c7b742e51d..fd38ef1fc0 100644
--- a/solutions/Virtual-Assistant/src/csharp/assistant/Dialogs/Shared/Resources/General.cs
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Resources/General.cs
@@ -1,5 +1,5 @@
//
-// Code generated by LUISGen C:\Users\lamil\source\repos\CAISolutions\CustomAssistant\CustomAssistant\DeploymentScripts\msbotClone\120.luis -cs Luis.General -o C:\Users\lamil\source\repos\CAISolutions\CustomAssistant\CustomAssistant\DeploymentScripts\..\Dialogs\Shared\Resources
+// Code generated by LUISGen C:\Users\lamil\source\repos\CAISolutions\NewsSkill\NewsSkill\DeploymentScripts\msbotClone\120.luis -cs Luis.General -o C:\Users\lamil\source\repos\CAISolutions\NewsSkill\NewsSkill\DeploymentScripts\..\Dialogs\Shared\Resources
// Tool github: https://github.com/microsoft/botbuilder-tools
// Changes may cause incorrect behavior and will be lost if the code is
// regenerated.
@@ -22,6 +22,7 @@ public enum Intent
Goodbye,
Greeting,
Help,
+ Logout,
Next,
None,
Restart
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillConfiguration.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillConfiguration.cs
new file mode 100644
index 0000000000..620154567b
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillConfiguration.cs
@@ -0,0 +1,87 @@
+using Microsoft.ApplicationInsights;
+using Microsoft.ApplicationInsights.Extensibility;
+using Microsoft.Bot.Builder.AI.Luis;
+using Microsoft.Bot.Builder.AI.QnA;
+using Microsoft.Bot.Builder.Azure;
+using Microsoft.Bot.Configuration;
+using System.Collections.Generic;
+
+namespace Microsoft.Bot.Solutions.Skills
+{
+ public class SkillConfiguration
+ {
+ public SkillConfiguration()
+ {
+
+ }
+
+ public SkillConfiguration(BotConfiguration botConfiguration, string[] parameters, Dictionary configuration)
+ {
+ foreach (var service in botConfiguration.Services)
+ {
+ switch (service.Type)
+ {
+ case ServiceTypes.AppInsights:
+ {
+ var appInsights = service as AppInsightsService;
+ var telemetryConfig = new TelemetryConfiguration(appInsights.InstrumentationKey);
+ TelemetryClient = new TelemetryClient(telemetryConfig);
+ break;
+ }
+
+ case ServiceTypes.Luis:
+ {
+ var luis = service as LuisService;
+ var luisApp = new LuisApplication(luis.AppId, luis.SubscriptionKey, luis.GetEndpoint());
+ LuisServices.Add(service.Id, new LuisRecognizer(luisApp));
+ break;
+ }
+
+ case ServiceTypes.Generic:
+ {
+ if (service.Name == "Authentication")
+ {
+ var authentication = service as GenericService;
+
+ if (!string.IsNullOrEmpty(authentication.Configuration["Azure Active Directory v2"]))
+ {
+ AuthConnectionName = authentication.Configuration["Azure Active Directory v2"];
+ }
+ }
+
+ break;
+ }
+ }
+ }
+
+ if(parameters != null)
+ {
+ // add the parameters the skill needs
+ foreach (var parameter in parameters)
+ {
+ // Initialize each parameter to null. Needs to be set later by the bot.
+ Properties.Add(parameter, null);
+ }
+ }
+
+ if(configuration != null)
+ {
+ // add the additional keys the skill needs
+ foreach (var set in configuration)
+ {
+ Properties.Add(set.Key, set.Value);
+ }
+ }
+ }
+
+ public string AuthConnectionName { get; set; }
+
+ public TelemetryClient TelemetryClient { get; set; }
+
+ public CosmosDbStorageOptions CosmosDbOptions { get; set; }
+
+ public Dictionary LuisServices { get; set; } = new Dictionary();
+
+ public Dictionary Properties { get; set; } = new Dictionary();
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillService.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillDefinition.cs
similarity index 73%
rename from solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillService.cs
rename to solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillDefinition.cs
index 4bec232294..0101729483 100644
--- a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillService.cs
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillDefinition.cs
@@ -4,9 +4,9 @@
namespace Microsoft.Bot.Solutions.Skills
{
- public class SkillService : ConnectedService
+ public class SkillDefinition : ConnectedService
{
- public SkillService() : base("skill")
+ public SkillDefinition() : base("skill")
{
}
@@ -16,11 +16,8 @@ public SkillService() : base("skill")
[JsonProperty("assembly")]
public string Assembly { get; set; }
- [JsonProperty("authConnectionName")]
- public string AuthConnectionName { get; set; }
-
- [JsonProperty("luisServiceId")]
- public string LuisServiceId { get; set; }
+ [JsonProperty("luisServiceIds")]
+ public string[] LuisServiceIds { get; set; }
[JsonProperty("parameters")]
public string[] Parameters { get; set; }
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillDialog.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillDialog.cs
index 08dbe3e32b..ca828eb8e5 100644
--- a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillDialog.cs
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillDialog.cs
@@ -1,5 +1,4 @@
-using Microsoft.ApplicationInsights;
-using Microsoft.Bot.Builder;
+using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Azure;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
@@ -17,194 +16,212 @@ public class SkillDialog : Dialog
private const string ActiveSkillStateKey = "ActiveSkill";
private const string TokenRequestEventName = "tokens/request";
private const string TokenResponseEventName = "tokens/response";
- private const string SkillBeginEventName = "skillBegin";
// Fields
+ private static Dictionary _skills;
+ private IStatePropertyAccessor _accessor;
+ private DialogSet _dialogs;
private InProcAdapter _inProcAdapter;
private IBot _activatedSkill;
- private CosmosDbStorageOptions _cosmosDbOptions;
- private TelemetryClient _telemetryClient;
- private SkillService _skill;
- private OAuthPrompt _authPrompt;
- private bool skillInitialized = false;
+ private bool _skillInitialized;
-
- public SkillDialog(CosmosDbStorageOptions cosmosDbOptions, TelemetryClient telemetryClient)
+ public SkillDialog(Dictionary skills, IStatePropertyAccessor accessor)
: base(nameof(SkillDialog))
{
- _cosmosDbOptions = cosmosDbOptions;
- _telemetryClient = telemetryClient;
+ _skills = skills;
+ _accessor = accessor;
}
- public override async Task BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
+ public override Task BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
{
- var skillDialogOptions = (SkillDialogOptions)options;
-
- _skill = skillDialogOptions.MatchedSkill;
+ var skillOptions = (SkillDialogOptions)options;
- // Set our active Skill so later methods know which Skill to use.
- dc.ActiveDialog.State[ActiveSkillStateKey] = skillDialogOptions.MatchedSkill;
+ // Save the active skill in state
+ var skillDefinition = skillOptions.SkillDefinition;
+ dc.ActiveDialog.State[ActiveSkillStateKey] = skillDefinition;
- _authPrompt = new OAuthPrompt(nameof(OAuthPrompt), new OAuthPromptSettings()
- {
- ConnectionName = skillDialogOptions.MatchedSkill.AuthConnectionName,
- Title = "Skill Authentication",
- Text = $"Please login to access this feature.",
- });
+ // Set parameters
+ var skillConfiguration = _skills[skillDefinition.Id];
- var parameters = new Dictionary();
- if (skillDialogOptions.MatchedSkill.Parameters != null)
+ if (skillDefinition.Parameters != null)
{
- foreach (var parameter in skillDialogOptions.MatchedSkill.Parameters)
+ foreach (var parameter in skillDefinition.Parameters)
{
- if (skillDialogOptions.Parameters.TryGetValue(parameter, out var paramValue))
+ if (skillOptions.Parameters.TryGetValue(parameter, out var paramValue))
{
- parameters.Add(parameter, paramValue);
+ skillConfiguration.Properties.Add(parameter, paramValue);
}
}
}
- var skillMetadata = new SkillMetadata(
- skillDialogOptions.LuisResult,
- skillDialogOptions.LuisService,
- skillDialogOptions.MatchedSkill.Configuration,
- parameters);
-
- var dialogBeginEvent = new Activity(
- type: ActivityTypes.Event,
- channelId: dc.Context.Activity.ChannelId,
- from: new ChannelAccount(id: dc.Context.Activity.From.Id, name: dc.Context.Activity.From.Name),
- recipient: new ChannelAccount(id: dc.Context.Activity.Recipient.Id, name: dc.Context.Activity.Recipient.Name),
- conversation: new ConversationAccount(id: dc.Context.Activity.Conversation.Id),
- name: SkillBeginEventName,
- value: skillMetadata);
-
- return await ForwardToSkill(dc, dialogBeginEvent);
+ // Initialize authentication prompt
+ _dialogs = new DialogSet(_accessor);
+ _dialogs.Add(new OAuthPrompt(nameof(OAuthPrompt), new OAuthPromptSettings()
+ {
+ ConnectionName = skillConfiguration.AuthConnectionName,
+ Title = "Skill Authentication",
+ Text = $"Please login to access this feature.",
+ }));
+
+ return Task.FromResult(EndOfTurn);
}
- public override Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
+ public override async Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
{
- return ForwardToSkill(dc, dc.Context.Activity);
+ var result = await ForwardToSkill(dc, dc.Context.Activity);
+ return result;
}
- private bool InitializeSkill(DialogContext dc)
+ private async Task InitializeSkill(DialogContext dc)
{
- if (dc.ActiveDialog.State.ContainsKey(ActiveSkillStateKey))
+ try
{
- var skill = dc.ActiveDialog.State[ActiveSkillStateKey] as SkillService;
+ var skillDefinition = dc.ActiveDialog.State[ActiveSkillStateKey] as SkillDefinition;
+ var skillConfiguration = _skills[skillDefinition.Id];
- var cosmosDbOptions = _cosmosDbOptions;
- cosmosDbOptions.CollectionId = skill.Name;
+ var cosmosDbOptions = skillConfiguration.CosmosDbOptions;
+ cosmosDbOptions.CollectionId = skillDefinition.Name;
+
+ // Initialize skill state
var cosmosDbStorage = new CosmosDbStorage(cosmosDbOptions);
+ var userState = new UserState(cosmosDbStorage);
var conversationState = new ConversationState(cosmosDbStorage);
+ // Create skill instance
try
{
- var skillType = Type.GetType(skill.Assembly);
- _activatedSkill = (IBot)Activator.CreateInstance(skillType, conversationState, $"{skill.Name}State", skill.Configuration);
+ var skillType = Type.GetType(skillDefinition.Assembly);
+ _activatedSkill = (IBot)Activator.CreateInstance(skillType, skillConfiguration, conversationState, userState, null, true);
}
catch (Exception e)
{
- var message = $"Skill ({skill.Name}) Type could not be created.";
+ var message = $"Skill ({skillDefinition.Name}) could not be created.";
throw new InvalidOperationException(message, e);
}
_inProcAdapter = new InProcAdapter
{
+ // set up skill turn error handling
OnTurnError = async (context, exception) =>
{
- await dc.Context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: exception.Message));
await context.SendActivityAsync(context.Activity.CreateReply($"Sorry, something went wrong trying to communicate with the skill. Please try again."));
- await dc.Context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"Skill Error: {exception.Message} | {exception.StackTrace}"));
- _telemetryClient.TrackException(exception);
+
+ // Send error trace to emulator
+ await dc.Context.SendActivityAsync(
+ new Activity(
+ type: ActivityTypes.Trace,
+ text: $"Skill Error: {exception.Message} | {exception.StackTrace}"
+ ));
+
+ // Log exception in AppInsights
+ skillConfiguration.TelemetryClient.TrackException(exception);
},
};
_inProcAdapter.Use(new EventDebuggerMiddleware());
- _inProcAdapter.Use(new AutoSaveStateMiddleware(conversationState));
-
- skillInitialized = true;
+ _inProcAdapter.Use(new AutoSaveStateMiddleware(userState, conversationState));
+ _skillInitialized = true;
}
- else
+ catch
{
- // No active skill?
+ // something went wrong initializing the skill, so end dialog cleanly and throw so the error is logged
+ _skillInitialized = false;
+ await dc.EndDialogAsync();
+ throw;
}
-
- return skillInitialized;
}
public async Task ForwardToSkill(DialogContext dc, Activity activity)
{
- if (!skillInitialized)
- {
- InitializeSkill(dc);
- }
-
- _inProcAdapter.ProcessActivity(activity, async (skillContext, ct) =>
+ try
{
- await _activatedSkill.OnTurnAsync(skillContext);
- }).Wait();
-
- var queue = new List();
- var endOfConversation = false;
- var skillResponse = _inProcAdapter.GetNextReply();
-
- while (skillResponse != null)
- {
- if (skillResponse.Type == ActivityTypes.EndOfConversation)
+ if (!_skillInitialized)
{
- endOfConversation = true;
+ await InitializeSkill(dc);
}
- else if (skillResponse?.Name == TokenRequestEventName)
+
+ _inProcAdapter.ProcessActivity(activity, async (skillContext, ct) =>
{
- // Send trace to emulator
- await dc.Context.SendActivityAsync(
- new Activity(
- type: ActivityTypes.Trace,
- text: $"<--Received a Token Request from a skill"
- ));
+ await _activatedSkill.OnTurnAsync(skillContext);
+ }).Wait();
- // Uncomment this line to prompt user for login every time the skill requests a token
- // var a = dc.Context.Adapter as BotFrameworkAdapter;
- // await a.SignOutUserAsync(dc.Context, _skill.AuthConnectionName, dc.Context.Activity.From.Id, default(CancellationToken));
+ var queue = new List();
+ var endOfConversation = false;
+ var skillResponse = _inProcAdapter.GetNextReply();
- var authResult = await _authPrompt.BeginDialogAsync(dc);
- if (authResult.Result?.GetType() == typeof(TokenResponse))
+ while (skillResponse != null)
+ {
+ if (skillResponse.Type == ActivityTypes.EndOfConversation)
{
- var tokenEvent = skillResponse.CreateReply();
- tokenEvent.Type = ActivityTypes.Event;
- tokenEvent.Name = TokenResponseEventName;
- tokenEvent.Value = authResult.Result;
-
- return await ForwardToSkill(dc, tokenEvent);
+ endOfConversation = true;
+ }
+ else if (skillResponse?.Name == TokenRequestEventName)
+ {
+ // Send trace to emulator
+ await dc.Context.SendActivityAsync(
+ new Activity(
+ type: ActivityTypes.Trace,
+ text: $"<--Received a Token Request from a skill"
+ ));
+
+ // Uncomment this line to prompt user for login every time the skill requests a token
+ //var a = dc.Context.Adapter as BotFrameworkAdapter;
+ //var skillDefinition = dc.ActiveDialog.State[ActiveSkillStateKey] as SkillDefinition;
+ //var skillConfiguration = _skills[skillDefinition.Id];
+ //await a.SignOutUserAsync(dc.Context, skillConfiguration.AuthConnectionName, dc.Context.Activity.From.Id, default(CancellationToken));
+
+ var innerDc = await _dialogs.CreateContextAsync(dc.Context);
+ var authResult = await innerDc.BeginDialogAsync(nameof(OAuthPrompt));
+ if (authResult.Result?.GetType() == typeof(TokenResponse))
+ {
+ var tokenEvent = skillResponse.CreateReply();
+ tokenEvent.Type = ActivityTypes.Event;
+ tokenEvent.Name = TokenResponseEventName;
+ tokenEvent.Value = authResult.Result;
+
+ return await ForwardToSkill(dc, tokenEvent);
+ }
+ else
+ {
+ return authResult;
+ }
}
else
{
- return authResult;
+ queue.Add(skillResponse);
}
+
+ skillResponse = _inProcAdapter.GetNextReply();
}
- else
+
+ // send skill queue to User
+ if (queue.Count > 0)
{
- queue.Add(skillResponse);
+ await dc.Context.SendActivitiesAsync(queue.ToArray());
}
- skillResponse = _inProcAdapter.GetNextReply();
- }
-
- // send skill queue to User
- if (queue.Count > 0)
- {
- await dc.Context.SendActivitiesAsync(queue.ToArray());
- }
+ // handle ending the skill conversation
+ if (endOfConversation)
+ {
+ await dc.Context.SendActivityAsync(
+ new Activity(
+ type: ActivityTypes.Trace,
+ text: $"<--Ending the skill conversation"
+ ));
- // handle ending the skill conversation
- if (endOfConversation)
- {
- return await dc.EndDialogAsync();
+ return await dc.EndDialogAsync();
+ }
+ else
+ {
+ return EndOfTurn;
+ }
}
- else
+ catch
{
- return EndOfTurn;
+ // something went wrong forwarding to the skill, so end dialog cleanly and throw so the error is logged.
+ // NOTE: errors within the skill itself are handled by the OnTurnError handler on the adapter.
+ await dc.EndDialogAsync();
+ throw;
}
}
}
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillDialogOptions.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillDialogOptions.cs
index b23019968a..1f50dc5515 100644
--- a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillDialogOptions.cs
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillDialogOptions.cs
@@ -7,11 +7,7 @@ namespace Microsoft.Bot.Solutions.Skills
{
public class SkillDialogOptions
{
- public SkillService MatchedSkill { get; set; }
-
- public LuisService LuisService { get; set; }
-
- public IRecognizerConvert LuisResult { get; set; }
+ public SkillDefinition SkillDefinition { get; set; }
public Dictionary Parameters { get; set; } = new Dictionary();
}
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillMetadata.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillMetadata.cs
deleted file mode 100644
index c07463cf01..0000000000
--- a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillMetadata.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using Microsoft.Bot.Builder;
-using Microsoft.Bot.Configuration;
-using System.Collections.Generic;
-
-namespace Microsoft.Bot.Solutions.Skills
-{
- ///
- /// Skills are invoked "in-process" at this time. We have already performed the Luis evaluation so pass this on to avoid duplication
- /// Skills don't have access to their own configuration file so we enable transfer of settings from the Skill registration
- /// Skills also need user information to personalise the experience, for example Timezone or Location. This is all under strict control
- /// by the Custom Assistant developer.
- ///
- public class SkillMetadata
- {
- public SkillMetadata(IRecognizerConvert luisResult, LuisService luisService, Dictionary configuration, Dictionary parameters)
- {
- LuisResult = luisResult;
- LuisService = luisService;
- Parameters = parameters;
- }
-
- public Dictionary Configuration { get; set; }
- public Dictionary Parameters { get; set; }
- public IRecognizerConvert LuisResult { get; set; }
- public LuisService LuisService { get; set; }
- }
-}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillRegistration.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillRegistration.cs
deleted file mode 100644
index ad7511255d..0000000000
--- a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillRegistration.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Collections.Generic;
-
-namespace Microsoft.Bot.Solutions.Skills
-{
- public class SkillRegistration
- {
- public string Name { get; set; }
-
- public string Description { get; set; }
-
- public string DispatcherModelName { get; set; }
-
- public string Assembly { get; set; }
-
- public string AuthConnectionName { get; set; }
-
- public string[] Parameters { get; set; }
-
- public Dictionary Configuration {get;set;}
- }
-}
diff --git a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillRouter.cs b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillRouter.cs
index c34619255b..9c533d9760 100644
--- a/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillRouter.cs
+++ b/solutions/Virtual-Assistant/src/csharp/microsoft.bot.solutions/Skills/SkillRouter.cs
@@ -5,17 +5,17 @@
public class SkillRouter
{
- private List _registeredSkills;
+ private List _registeredSkills;
- public SkillRouter(List registeredSkills)
+ public SkillRouter(List registeredSkills)
{
// Retrieve any Skills that have been registered with the Bot
_registeredSkills = registeredSkills;
}
- public SkillService IdentifyRegisteredSkill(string skillName)
+ public SkillDefinition IdentifyRegisteredSkill(string skillName)
{
- SkillService matchedSkill = null;
+ SkillDefinition matchedSkill = null;
// Did we find any skills?
if (_registeredSkills != null)
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/CalendarSkill.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/CalendarSkill.cs
new file mode 100644
index 0000000000..ea135f3c72
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/CalendarSkill.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Bot.Builder;
+using Microsoft.Bot.Builder.Dialogs;
+using Microsoft.Bot.Schema;
+using Microsoft.Bot.Solutions.Skills;
+
+namespace CalendarSkill
+{
+ ///
+ /// Main entry point and orchestration for bot.
+ ///
+ public class CalendarSkill : IBot
+ {
+ private bool _skillMode;
+ private readonly SkillConfiguration _services;
+ private readonly UserState _userState;
+ private readonly ConversationState _conversationState;
+ private readonly IServiceManager _serviceManager;
+ private DialogSet _dialogs;
+
+ public CalendarSkill(SkillConfiguration services, ConversationState conversationState, UserState userState, IServiceManager serviceManager = null, bool skillMode = false)
+ {
+ _skillMode = skillMode;
+ _services = services ?? throw new ArgumentNullException(nameof(services));
+ _userState = userState ?? throw new ArgumentNullException(nameof(userState));
+ _conversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
+ _serviceManager = serviceManager ?? new ServiceManager();
+
+ _dialogs = new DialogSet(_conversationState.CreateProperty(nameof(DialogState)));
+ _dialogs.Add(new MainDialog(_services, _conversationState, _userState, _serviceManager, _skillMode));
+ }
+
+
+ ///
+ /// Run every turn of the conversation. Handles orchestration of messages.
+ ///
+ /// Bot Turn Context.
+ /// Task CancellationToken.
+ /// A representing the asynchronous operation.
+ public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken)
+ {
+ var dc = await _dialogs.CreateContextAsync(turnContext);
+ var result = await dc.ContinueDialogAsync();
+
+ if (result.Status == DialogTurnStatus.Empty)
+ {
+ if (!_skillMode)
+ {
+ // if localMode, check for conversation update from user before starting dialog
+ if (turnContext.Activity.Type == ActivityTypes.ConversationUpdate)
+ {
+ var activity = turnContext.Activity.AsConversationUpdateActivity();
+
+ // if conversation update is not from the bot.
+ if (!activity.MembersAdded.Any(m => m.Id == activity.Recipient.Id))
+ {
+ await dc.BeginDialogAsync(nameof(MainDialog));
+ }
+ }
+ }
+ else
+ {
+ // if skillMode, begin dialog
+ await dc.BeginDialogAsync(nameof(MainDialog));
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Main/CalendarSkillState.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/CalendarSkillState.cs
similarity index 86%
rename from solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Main/CalendarSkillState.cs
rename to solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/CalendarSkillState.cs
index b3ca6538c1..37ccc836a3 100644
--- a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Main/CalendarSkillState.cs
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/CalendarSkillState.cs
@@ -1,24 +1,13 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT license.
-
+using Microsoft.Graph;
using System;
using System.Collections.Generic;
-using Microsoft.Bot.Builder;
-using Microsoft.Bot.Builder.Dialogs;
-using Microsoft.Graph;
namespace CalendarSkill
{
- ///
- /// The state of calendar skill.
- ///
public class CalendarSkillState
{
public const int PageSize = 5;
- ///
- /// Initializes a new instance of the class.
- ///
public CalendarSkillState()
{
User = new User();
@@ -48,6 +37,8 @@ public CalendarSkillState()
public UserInformation UserInfo { get; set; }
+ public Luis.Calendar LuisResult { get; set; }
+
public string Title { get; set; }
public string Content { get; set; }
@@ -80,10 +71,6 @@ public CalendarSkillState()
public int ShowAttendeesIndex { get; set; }
- public IRecognizerConvert LuisResultPassedFromSkill { get; set; }
-
- public DialogState ConversationDialogState { get; set; }
-
public int ShowEventIndex { get; set; }
public List SummaryEvents { get; set; }
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/demoskill/Connected Services/Application Insights/ConnectedService.json b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Connected Services/Application Insights/ConnectedService.json
similarity index 100%
rename from solutions/Virtual-Assistant/src/csharp/skills/demoskill/Connected Services/Application Insights/ConnectedService.json
rename to solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Connected Services/Application Insights/ConnectedService.json
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/DeploymentScripts/msbotClone/bot.recipe b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/DeploymentScripts/msbotClone/bot.recipe
new file mode 100644
index 0000000000..3c97b61e8a
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/DeploymentScripts/msbotClone/bot.recipe
@@ -0,0 +1,50 @@
+{
+ "version": "1.0",
+ "resources": [
+ {
+ "type": "endpoint",
+ "id": "1",
+ "name": "development",
+ "url": "http://localhost:3980/api/messages"
+ },
+ {
+ "type": "endpoint",
+ "id": "108",
+ "name": "production",
+ "url": "https://your-bot-url.azurewebsites.net/api/messages"
+ },
+ {
+ "type": "abs",
+ "id": "199",
+ "name": "ABS"
+ },
+ {
+ "type": "blob",
+ "id": "2",
+ "name": "AzStorage",
+ "container": "transcripts"
+ },
+ {
+ "type": "appInsights",
+ "id": "3",
+ "name": "AppInsights"
+ },
+ {
+ "type": "cosmosdb",
+ "id": "8",
+ "name": "CosmosDB",
+ "database": "botstate-db",
+ "collection": "botstate-collection"
+ },
+ {
+ "type": "luis",
+ "id": "general",
+ "name": "General"
+ },
+ {
+ "type": "luis",
+ "id": "calendar",
+ "name": "Calendar"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/CognitiveModels/LUISModel.json b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/DeploymentScripts/msbotClone/calendar.luis
similarity index 100%
rename from solutions/Virtual-Assistant/src/csharp/skills/calendarskill/CognitiveModels/LUISModel.json
rename to solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/DeploymentScripts/msbotClone/calendar.luis
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/DeploymentScripts/msbotClone/general.luis b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/DeploymentScripts/msbotClone/general.luis
new file mode 100644
index 0000000000..517a361102
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/DeploymentScripts/msbotClone/general.luis
@@ -0,0 +1,501 @@
+{
+ "name": "General",
+ "versionId": "0.1",
+ "desc": "",
+ "culture": "en-us",
+ "intents": [
+ {
+ "name": "Cancel"
+ },
+ {
+ "name": "ConfirmMore"
+ },
+ {
+ "name": "ConfirmNo"
+ },
+ {
+ "name": "ConfirmYes"
+ },
+ {
+ "name": "Escalate"
+ },
+ {
+ "name": "Goodbye"
+ },
+ {
+ "name": "Greeting"
+ },
+ {
+ "name": "Help"
+ },
+ {
+ "name": "Next"
+ },
+ {
+ "name": "None"
+ },
+ {
+ "name": "Restart"
+ }
+ ],
+ "entities": [],
+ "closedLists": [],
+ "composites": [],
+ "patternAnyEntities": [],
+ "regex_entities": [],
+ "prebuiltEntities": [
+ {
+ "name": "datetimeV2",
+ "roles": []
+ },
+ {
+ "name": "number",
+ "roles": []
+ }
+ ],
+ "regex_features": [],
+ "model_features": [],
+ "patterns": [],
+ "utterances": [
+ {
+ "text": "abort",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "absolutely",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "advance",
+ "intent": "Next",
+ "entities": []
+ },
+ {
+ "text": "by no means",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "bye",
+ "intent": "Goodbye",
+ "entities": []
+ },
+ {
+ "text": "can i do that again",
+ "intent": "Restart",
+ "entities": []
+ },
+ {
+ "text": "can i talk to a person",
+ "intent": "Escalate",
+ "entities": []
+ },
+ {
+ "text": "can you help me",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "cancel",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "contact support",
+ "intent": "Escalate",
+ "entities": []
+ },
+ {
+ "text": "definitely",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "disregard",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "don't",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "don't do it",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "don't do that",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "for sure",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "go back",
+ "intent": "Restart",
+ "entities": []
+ },
+ {
+ "text": "go forward",
+ "intent": "Next",
+ "entities": []
+ },
+ {
+ "text": "go to next",
+ "intent": "Next",
+ "entities": []
+ },
+ {
+ "text": "good evening",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "good morning",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "good night",
+ "intent": "Goodbye",
+ "entities": []
+ },
+ {
+ "text": "goodbye",
+ "intent": "Goodbye",
+ "entities": []
+ },
+ {
+ "text": "hello",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "hello bot",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "help",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "help me",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "help please",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "hi",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "hiya",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "how are you",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "how are you today",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "i don't understand",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "i need to change something",
+ "intent": "Restart",
+ "entities": []
+ },
+ {
+ "text": "i think so",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "i want to talk to a human",
+ "intent": "Escalate",
+ "entities": []
+ },
+ {
+ "text": "i would like to cancel",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "i would like to know more",
+ "intent": "ConfirmMore",
+ "entities": []
+ },
+ {
+ "text": "i'm stuck",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "let's do it",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "let's do that",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "luhan",
+ "intent": "None",
+ "entities": []
+ },
+ {
+ "text": "more",
+ "intent": "ConfirmMore",
+ "entities": []
+ },
+ {
+ "text": "more info",
+ "intent": "ConfirmMore",
+ "entities": []
+ },
+ {
+ "text": "more information",
+ "intent": "ConfirmMore",
+ "entities": []
+ },
+ {
+ "text": "my water bottle is green",
+ "intent": "None",
+ "entities": []
+ },
+ {
+ "text": "nah",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "never mind",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "next",
+ "intent": "Next",
+ "entities": []
+ },
+ {
+ "text": "next item",
+ "intent": "Next",
+ "entities": []
+ },
+ {
+ "text": "no",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "no thanks",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "no that's okay",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "no way",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "nope",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "not at all",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "not really",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "not right now thanks",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "oh yes",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "ok",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "ok bye",
+ "intent": "Goodbye",
+ "entities": []
+ },
+ {
+ "text": "ok thanks",
+ "intent": "Goodbye",
+ "entities": []
+ },
+ {
+ "text": "okay",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "paper planes in the sky",
+ "intent": "None",
+ "entities": []
+ },
+ {
+ "text": "please stop",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "restart",
+ "intent": "Restart",
+ "entities": []
+ },
+ {
+ "text": "show me more",
+ "intent": "ConfirmMore",
+ "entities": []
+ },
+ {
+ "text": "show next",
+ "intent": "Next",
+ "entities": []
+ },
+ {
+ "text": "start over",
+ "intent": "Restart",
+ "entities": []
+ },
+ {
+ "text": "stop",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "sure",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "sure thing",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "talk to a human",
+ "intent": "Escalate",
+ "entities": []
+ },
+ {
+ "text": "that's it thanks",
+ "intent": "Goodbye",
+ "entities": []
+ },
+ {
+ "text": "transfer me please",
+ "intent": "Escalate",
+ "entities": []
+ },
+ {
+ "text": "turtles like to swim in the ocean",
+ "intent": "None",
+ "entities": []
+ },
+ {
+ "text": "undo",
+ "intent": "Restart",
+ "entities": []
+ },
+ {
+ "text": "what can i say",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "what can you do",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "what can you help me with",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "what do i do now",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "why doesn't this work",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "why not",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "yeah",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "yeah that would be great thanks",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "yep that's right",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "yes please",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "yup",
+ "intent": "ConfirmYes",
+ "entities": []
+ }
+ ]
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Cancel/CancelDialog.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Cancel/CancelDialog.cs
new file mode 100644
index 0000000000..8d667b8566
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Cancel/CancelDialog.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Bot.Builder.Dialogs;
+
+namespace CalendarSkill
+{
+ public class CancelDialog : ComponentDialog
+ {
+ // Constants
+ public const string CancelPrompt = "cancelPrompt";
+
+ // Fields
+ private static CancelResponses _responder = new CancelResponses();
+
+ public CancelDialog()
+ : base(nameof(CancelDialog))
+ {
+ InitialDialogId = nameof(CancelDialog);
+
+ var cancel = new WaterfallStep[]
+ {
+ AskToCancel,
+ FinishCancelDialog,
+ };
+
+ AddDialog(new WaterfallDialog(InitialDialogId, cancel));
+ AddDialog(new ConfirmPrompt(CancelPrompt));
+ }
+
+ public static async Task AskToCancel(WaterfallStepContext sc, CancellationToken cancellationToken) => await sc.PromptAsync(CancelPrompt, new PromptOptions()
+ {
+ Prompt = await _responder.RenderTemplate(sc.Context, "en", CancelResponses._confirmPrompt),
+ });
+
+ public static async Task FinishCancelDialog(WaterfallStepContext sc, CancellationToken cancellationToken) => await sc.EndDialogAsync((bool)sc.Result);
+
+ protected override async Task EndComponentAsync(DialogContext outerDc, object result, CancellationToken cancellationToken)
+ {
+ var doCancel = (bool)result;
+
+ if (doCancel)
+ {
+ // If user chose to cancel
+ await _responder.ReplyWith(outerDc.Context, CancelResponses._cancelConfirmed);
+
+ // Cancel all in outer stack of component i.e. the stack the component belongs to
+ return await outerDc.CancelAllDialogsAsync();
+ }
+ else
+ {
+ // else if user chose not to cancel
+ await _responder.ReplyWith(outerDc.Context, CancelResponses._cancelDenied);
+
+ // End this component. Will trigger reprompt/resume on outer stack
+ return await outerDc.EndDialogAsync();
+ }
+ }
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Cancel/CancelResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Cancel/CancelResponses.cs
new file mode 100644
index 0000000000..fd09b9817e
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Cancel/CancelResponses.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using CalendarSkill.Dialogs.Cancel.Resources;
+using Microsoft.Bot.Builder.TemplateManager;
+
+namespace CalendarSkill
+{
+ public class CancelResponses : TemplateManager
+ {
+ // Constants
+ public const string _confirmPrompt = "Cancel.ConfirmCancelPrompt";
+ public const string _cancelConfirmed = "Cancel.CancelConfirmed";
+ public const string _cancelDenied = "Cancel.CancelDenied";
+
+ // Fields
+ private static LanguageTemplateDictionary _responseTemplates = new LanguageTemplateDictionary
+ {
+ ["default"] = new TemplateIdMap
+ {
+ { _confirmPrompt, (context, data) => CancelStrings.CANCEL_PROMPT },
+ { _cancelConfirmed, (context, data) => CancelStrings.CANCEL_CONFIRMED },
+ { _cancelDenied, (context, data) => CancelStrings.CANCEL_DENIED },
+ },
+ ["en"] = new TemplateIdMap { },
+ ["fr"] = new TemplateIdMap { },
+ };
+
+ public CancelResponses()
+ {
+ Register(new DictionaryRenderer(_responseTemplates));
+ }
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Cancel/Resources/CancelStrings.Designer.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Cancel/Resources/CancelStrings.Designer.cs
new file mode 100644
index 0000000000..1f1dcc9401
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Cancel/Resources/CancelStrings.Designer.cs
@@ -0,0 +1,105 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace CalendarSkill.Dialogs.Cancel.Resources
+{
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class CancelStrings
+ {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal CancelStrings()
+ {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager
+ {
+ get
+ {
+ if (object.ReferenceEquals(resourceMan, null))
+ {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CalendarSkill.Dialogs.Cancel.Resources.CancelStrings", typeof(CancelStrings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Globalization.CultureInfo Culture
+ {
+ get
+ {
+ return resourceCulture;
+ }
+ set
+ {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Ok, let's start over..
+ ///
+ public static string CANCEL_CONFIRMED
+ {
+ get
+ {
+ return ResourceManager.GetString("CANCEL_CONFIRMED", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Ok, let's keep going..
+ ///
+ public static string CANCEL_DENIED
+ {
+ get
+ {
+ return ResourceManager.GetString("CANCEL_DENIED", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Are you sure you want to cancel?.
+ ///
+ public static string CANCEL_PROMPT
+ {
+ get
+ {
+ return ResourceManager.GetString("CANCEL_PROMPT", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/pointofinterestskill/Dialogs/Shared/Resources/BotStrings.resx b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Cancel/Resources/CancelStrings.resx
similarity index 94%
rename from solutions/Virtual-Assistant/src/csharp/skills/pointofinterestskill/Dialogs/Shared/Resources/BotStrings.resx
rename to solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Cancel/Resources/CancelStrings.resx
index 1af7de150c..8af874c85b 100644
--- a/solutions/Virtual-Assistant/src/csharp/skills/pointofinterestskill/Dialogs/Shared/Resources/BotStrings.resx
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Cancel/Resources/CancelStrings.resx
@@ -117,4 +117,13 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ Ok, let's start over.
+
+
+ Ok, let's keep going.
+
+
+ Are you sure you want to cancel?
+
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/CreateEvent/Resources/CreateEventResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/CreateEvent/Resources/CreateEventResponses.cs
new file mode 100644
index 0000000000..f172fab9f4
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/CreateEvent/Resources/CreateEventResponses.cs
@@ -0,0 +1,129 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace CalendarSkill.Dialogs.CreateEvent.Resources
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class CreateEventResponses
+ {
+ private const string JsonFileName = "CreateEventResponses.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+ public static BotResponse NoTitle => GetBotResponse();
+
+ public static BotResponse NoContent => GetBotResponse();
+
+ public static BotResponse NoLocation => GetBotResponse();
+
+ public static BotResponse ConfirmCreate => GetBotResponse();
+
+ public static BotResponse ConfirmCreateFailed => GetBotResponse();
+
+ public static BotResponse EventCreated => GetBotResponse();
+
+ public static BotResponse EventCreationFailed => GetBotResponse();
+
+ public static BotResponse NoAttendeesMS => GetBotResponse();
+
+ public static BotResponse WrongAddress => GetBotResponse();
+
+ public static BotResponse NoAttendees => GetBotResponse();
+
+ public static BotResponse PromptTooManyPeople => GetBotResponse();
+
+ public static BotResponse PromptPersonNotFound => GetBotResponse();
+
+ public static BotResponse NoStartDate => GetBotResponse();
+
+ public static BotResponse NoStartTime => GetBotResponse();
+
+ public static BotResponse NoDuration => GetBotResponse();
+
+ public static BotResponse FindUserErrorMessage => GetBotResponse();
+
+ public static BotResponse ConfirmRecipient => GetBotResponse();
+
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(CreateEventResponses).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\CreateEvent\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/CreateEvent/Resources/CreateEventResponses.en.json b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/CreateEvent/Resources/CreateEventResponses.en.json
new file mode 100644
index 0000000000..497bb657ac
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/CreateEvent/Resources/CreateEventResponses.en.json
@@ -0,0 +1,206 @@
+{
+ "NoTitle": {
+ "replies": [
+
+ {
+ "text": "Alright, I will send this to {UserName}, what is the subject of the meeting?",
+ "speak": "Alright, I will send this to {UserName}, what is the subject of the meeting?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoContent": {
+ "replies": [
+
+ {
+ "text": "What is the meeting title?",
+ "speak": "What is the meeting title?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoLocation": {
+ "replies": [
+
+ {
+ "text": "What location?",
+ "speak": "What location?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "ConfirmCreate": {
+ "replies": [
+ {
+ "text": "I'm ready to create it. Please confirm the following:",
+ "speak": "I'm ready to create it. Please confirm the following:"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "ConfirmCreateFailed": {
+ "replies": [
+ {
+ "text": "I'm ready to create it. Please confirm the following. Please confirm.",
+ "speak": "I'm ready to create it. Please confirm the following. Please confirm."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "EventCreated": {
+ "replies": [
+
+ {
+ "text": "I have added the following to your calendar ",
+ "speak": "I have added the following to your calendar "
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "EventCreationFailed": {
+ "replies": [
+
+ {
+ "text": "Event creation failed",
+ "speak": "Event creation failed"
+ },
+ {
+ "text": "Something went wrong, try again please.",
+ "speak": "Something went wrong, try again please."
+ },
+ {
+ "text": "It seems the event could not be created, please try again later.",
+ "speak": "It seems the event could not be created, please try again later."
+ },
+ {
+ "text": "Creation of the event failed, please try again.",
+ "speak": "Creation of the event failed, please try again."
+ },
+ {
+ "text": "An error occured with creating the event, want to try again?",
+ "speak": "An error occured with creating the event, want to try again?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoAttendeesMS": {
+ "replies": [
+
+ {
+ "text": "Who are the participants?",
+ "speak": "Who are the participants?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "WrongAddress": {
+ "replies": [
+
+ {
+ "text": "That doesn't look like an email address, please try again. (Please enter Email address like: Alice@example.com)",
+ "speak": "That doesn't look like an email address, please try again. (Please enter Email address like: Alice@example.com)"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoAttendees": {
+ "replies": [
+
+ {
+ "text": "Who are the participants? (Please enter Email address like: Alice@example.com)",
+ "speak": "Who are the participants? (Please enter Email address like: Alice@example.com)"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "PromptTooManyPeople": {
+ "replies": [
+ {
+ "text": "There are too many people called {UserName}, Please re-enter the name.",
+ "speak": "There are too many people called {UserName}, Please re-enter the name."
+ },
+ {
+ "text": "I found too many contacts with this name. Can you be a bit more specific?",
+ "speak": "I found too many contacts with this name. Can you be a bit more specific?"
+ },
+ {
+ "text": "Can you try again by stating their firstname and surname?",
+ "speak": "Can you try again by stating their firstname and surname?"
+ },
+ {
+ "text": "I have found too many contacts, can you try again by being more specific with the name? It helps if you include a surname.",
+ "speak": "I have found too many contacts, can you try again by being more specific with the name? It helps if you include a surname."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "PromptPersonNotFound": {
+ "replies": [
+ {
+ "text": "Person not found, please input the right name.",
+ "speak": "Person not found, please input the right name."
+ },
+ {
+ "text": "I cannot find the contact your are looking for right now. Please include more details, do you want to try again?",
+ "speak": "I cannot find the contact your are looking for right now. Please include more details, do you want to try again?"
+ },
+ {
+ "text": "Something is wrong because I cannot find the contact you are looking for. Please try again by stating out their name again. It helps if you include a surname.",
+ "speak": "Something is wrong because I cannot find the contact you are looking for. Please try again by stating out their name again. It helps if you include a surname."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoStartDate": {
+ "replies": [
+
+ {
+ "text": "What date?",
+ "speak": "What date?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoStartTime": {
+ "replies": [
+
+ {
+ "text": "What time?",
+ "speak": "What time?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoDuration": {
+ "replies": [
+
+ {
+ "text": "How long is the meeting for?",
+ "speak": "How long is the meeting for?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "FindUserErrorMessage": {
+ "replies": [
+ {
+ "text": "I can't find the person named {UserName} right now. Please try again or cancel.",
+ "speak": "I can't find the person named {UserName} right now. Please try again or cancel."
+ }
+ ],
+ "inputHint": "acceptingInput"
+ },
+ "ConfirmRecipient": {
+ "replies": [
+ {
+ "text": "Who do you want to invite?",
+ "speak": "Who do you want to invite?"
+ },
+ {
+ "text": "What contact do you want to select?",
+ "speak": "What contact do you want to select?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/ToDoSkill/Dialogs/Shared/Resources/ToDoBotResponses.tt b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/CreateEvent/Resources/CreateEventResponses.tt
similarity index 68%
rename from solutions/Virtual-Assistant/src/csharp/skills/ToDoSkill/Dialogs/Shared/Resources/ToDoBotResponses.tt
rename to solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/CreateEvent/Resources/CreateEventResponses.tt
index eaa0ff1316..274e537649 100644
--- a/solutions/Virtual-Assistant/src/csharp/skills/ToDoSkill/Dialogs/Shared/Resources/ToDoBotResponses.tt
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/CreateEvent/Resources/CreateEventResponses.tt
@@ -1,5 +1,5 @@
-<#@ assembly name="$(SolutionDir)Lib\Newtonsoft.Json.dll" #>
-<#@ template debug="false" hostspecific="true" language="C#" #>
+<#@ assembly name="Newtonsoft.Json.dll" #>
+<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#
var className = System.IO.Path.GetFileNameWithoutExtension(Host.TemplateFile);
@@ -7,65 +7,59 @@
string myFile = System.IO.File.ReadAllText(this.Host.ResolvePath(className + ".en.json"));
var json = Newtonsoft.Json.JsonConvert.DeserializeObject>(myFile);
#>
-//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
namespace <#= namespaceName #>
{
- using System;
- using System.Collections.Generic;
- using System.Globalization;
- using System.IO;
- using System.Linq;
- using System.Runtime.CompilerServices;
- using Microsoft.Bot.Solutions.Dialogs;
- using Newtonsoft.Json;
-
- ///
- /// ToDo bot responses class.
+ ///
+ /// Calendar bot responses class.
///
public static class <#= className #>
{
- private const string _jsonFileName = "<#=className#>.*.json";
+ private const string JsonFileName = "<#=className#>.*.json";
- private static Dictionary> _jsonResponses;
+ private static Dictionary> jsonResponses;
// Generated code:
- // This code runs in the text json:
-<# foreach (var propertyName in json) { #>
+ // This code runs in the text json:
+<# foreach (var propertyName in json) { #>
public static BotResponse <#= propertyName.Key.Substring(0, 1).ToUpperInvariant() + propertyName.Key.Substring(1) #> => GetBotResponse();
+
<# } #>
-
private static Dictionary> JsonResponses
{
get
{
- if (_jsonResponses != null)
+ if (jsonResponses != null)
{
- return _jsonResponses;
+ return jsonResponses;
}
- _jsonResponses = new Dictionary>();
+ jsonResponses = new Dictionary>();
var dir = Path.GetDirectoryName(typeof(<#= className #>).Assembly.Location);
- var resDir = Path.Combine(dir, "Dialogs\\Shared\\Resources");
+ var resDir = Path.Combine(dir, "Dialogs\\CreateEvent\\Resources");
- var jsonFiles = Directory.GetFiles(resDir, _jsonFileName);
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
foreach (var file in jsonFiles)
{
var jsonData = File.ReadAllText(file);
- var jsonResponses = JsonConvert.DeserializeObject>(jsonData);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
var key = new FileInfo(file).Name.Split(".")[1].ToLower();
- if (_jsonResponses.ContainsKey(key))
- {
- _jsonResponses[key] = jsonResponses;
- }
- else
- {
- _jsonResponses.Add(key, jsonResponses);
- }
+
+ jsonResponses.Add(key, responses);
}
- return _jsonResponses;
+ return jsonResponses;
}
}
@@ -98,8 +92,8 @@ namespace <#= namespaceName #>
{
if (JsonResponses.ContainsKey(locale))
{
- return JsonResponses[locale].ContainsKey(propertyName) ?
- JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
null;
}
@@ -111,5 +105,4 @@ namespace <#= namespaceName #>
}
}
}
-
}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/DeleteEvent/Resources/DeleteEventResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/DeleteEvent/Resources/DeleteEventResponses.cs
new file mode 100644
index 0000000000..40e79a740c
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/DeleteEvent/Resources/DeleteEventResponses.cs
@@ -0,0 +1,109 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace CalendarSkill.Dialogs.DeleteEvent.Resources
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class DeleteEventResponses
+ {
+ private const string JsonFileName = "DeleteEventResponses.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+ public static BotResponse ConfirmDelete => GetBotResponse();
+
+ public static BotResponse ConfirmDeleteFailed => GetBotResponse();
+
+ public static BotResponse EventDeleted => GetBotResponse();
+
+ public static BotResponse EventWithStartTimeNotFound => GetBotResponse();
+
+ public static BotResponse NoDeleteStartTime => GetBotResponse();
+
+ public static BotResponse NoUpdateStartTime => GetBotResponse();
+
+ public static BotResponse MultipleEventsStartAtSameTime => GetBotResponse();
+
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(DeleteEventResponses).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\DeleteEvent\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/DeleteEvent/Resources/DeleteEventResponses.en.json b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/DeleteEvent/Resources/DeleteEventResponses.en.json
new file mode 100644
index 0000000000..047346a955
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/DeleteEvent/Resources/DeleteEventResponses.en.json
@@ -0,0 +1,93 @@
+{
+ "ConfirmDelete": {
+ "replies": [
+ {
+ "text": "Are you sure you want to delete the following from your calendar?",
+ "speak": "Are you sure you want to delete the following from your calendar?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "ConfirmDeleteFailed": {
+ "replies": [
+ {
+ "text": "Are you sure you want to delete the following from your calendar? Please say 'yes' or 'no' or something like that.",
+ "speak": "Are you sure you want to delete the following from your calendar? Please say 'yes' or 'no' or something like that."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "EventDeleted": {
+ "replies": [
+ {
+ "text": "I have deleted the event.",
+ "speak": "I have deleted the event."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "EventWithStartTimeNotFound": {
+ "replies": [
+
+ {
+ "text": "No event start at this time or with that title, please try again.",
+ "speak": "No event start at this time or with that title, please try again."
+ },
+ {
+ "text": "I could not find an event you mentioned, please try again.",
+ "speak": "I could not find an event you mentioned, please try again."
+ },
+ {
+ "text": "No event can be found with the event time or title, can you try again?",
+ "speak": "No event can be found with the event time or title, can you try again?"
+ },
+ {
+ "text": "It seems there is no event with that name, try again please.",
+ "speak": "It seems there is no event with that name, try again please."
+ },
+ {
+ "text": "It seems there is no event at this time, please try again",
+ "speak": "It seems there is no event at this time, please try again"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoDeleteStartTime": {
+ "replies": [
+
+ {
+ "text": "What event would you like to delete? By communicating the start time or title I can retrieve your event.",
+ "speak": "What event would you like to delete? By communicating the start time or title I can retrieve your event."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoUpdateStartTime": {
+ "replies": [
+
+ {
+ "text": "What event would you like to adjust? By communicating the start time or title I can retrieve your event.",
+ "speak": "What event would you like to adjust? By communicating the start time or title I can retrieve your event."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "MultipleEventsStartAtSameTime": {
+ "replies": [
+
+ {
+ "text": "Multiple events found, please select the one you need.",
+ "speak": "Multiple events found, please select the one you need."
+ },
+ {
+ "text": "I have found multiple events, can you select the event of choice.",
+ "speak": "I have found multiple events, can you select the event of choice."
+ },
+ {
+ "text": "There are multiple events, please select the relevant event.",
+ "speak": "There are multiple events, please select the relevant event."
+ }
+ ],
+ "inputHint": "expectingInput"
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/DeleteEvent/Resources/DeleteEventResponses.tt b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/DeleteEvent/Resources/DeleteEventResponses.tt
new file mode 100644
index 0000000000..1eed631ce9
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/DeleteEvent/Resources/DeleteEventResponses.tt
@@ -0,0 +1,108 @@
+<#@ assembly name="Newtonsoft.Json.dll" #>
+<#@ template debug="false" hostspecific="true" language="C#" #>
+<#@ output extension=".cs" #>
+<#
+ var className = System.IO.Path.GetFileNameWithoutExtension(Host.TemplateFile);
+ var namespaceName = System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint");;
+ string myFile = System.IO.File.ReadAllText(this.Host.ResolvePath(className + ".en.json"));
+ var json = Newtonsoft.Json.JsonConvert.DeserializeObject>(myFile);
+#>
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace <#= namespaceName #>
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class <#= className #>
+ {
+ private const string JsonFileName = "<#=className#>.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+<# foreach (var propertyName in json) { #>
+ public static BotResponse <#= propertyName.Key.Substring(0, 1).ToUpperInvariant() + propertyName.Key.Substring(1) #> => GetBotResponse();
+
+<# } #>
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(<#= className #>).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\DeleteEvent\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Main/MainDialog.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Main/MainDialog.cs
new file mode 100644
index 0000000000..f3f62c8d38
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Main/MainDialog.cs
@@ -0,0 +1,274 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using CalendarSkill.Dialogs.Main.Resources;
+using CalendarSkill.Dialogs.Shared.Resources;
+using Luis;
+using Microsoft.Bot.Builder;
+using Microsoft.Bot.Builder.Dialogs;
+using Microsoft.Bot.Schema;
+using Microsoft.Bot.Solutions;
+using Microsoft.Bot.Solutions.Dialogs;
+using Microsoft.Bot.Solutions.Extensions;
+using Microsoft.Bot.Solutions.Skills;
+
+namespace CalendarSkill
+{
+ public class MainDialog : RouterDialog
+ {
+ private bool _skillMode;
+ private SkillConfiguration _services;
+ private UserState _userState;
+ private ConversationState _conversationState;
+ private IServiceManager _serviceManager;
+ private IStatePropertyAccessor _stateAccessor;
+ private CalendarSkillResponseBuilder _responseBuilder = new CalendarSkillResponseBuilder();
+
+ public MainDialog(
+ SkillConfiguration services,
+ ConversationState conversationState,
+ UserState userState,
+ IServiceManager serviceManager,
+ bool skillMode)
+ : base(nameof(MainDialog))
+ {
+ _skillMode = skillMode;
+ _services = services;
+ _userState = userState;
+ _conversationState = conversationState;
+ _serviceManager = serviceManager;
+
+ // Initialize state accessor
+ _stateAccessor = _conversationState.CreateProperty(nameof(CalendarSkillState));
+
+ // Register dialogs
+ RegisterDialogs();
+ }
+
+ protected override async Task OnStartAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (!_skillMode)
+ {
+ // send a greeting if we're in local mode
+ await dc.Context.SendActivityAsync(dc.Context.Activity.CreateReply(CalendarMainResponses.CalendarWelcomeMessage));
+ }
+ }
+
+ protected override async Task RouteAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var state = await _stateAccessor.GetAsync(dc.Context, () => new CalendarSkillState());
+
+ // If dispatch result is general luis model
+ _services.LuisServices.TryGetValue("calendar", out var luisService);
+
+ if (luisService == null)
+ {
+ throw new Exception("The specified LUIS Model could not be found in your Bot Services configuration.");
+ }
+ else
+ {
+ var result = await luisService.RecognizeAsync(dc.Context, CancellationToken.None);
+ var intent = result?.TopIntent().intent;
+
+ var skillOptions = new CalendarSkillDialogOptions
+ {
+ SkillMode = _skillMode,
+ };
+
+ // switch on general intents
+ switch (intent)
+ {
+ case Calendar.Intent.FindMeetingRoom:
+ case Calendar.Intent.CreateCalendarEntry:
+ {
+ await dc.BeginDialogAsync(nameof(CreateEventDialog), skillOptions);
+ break;
+ }
+
+ case Calendar.Intent.DeleteCalendarEntry:
+ {
+ await dc.BeginDialogAsync(nameof(DeleteEventDialog), skillOptions);
+ break;
+ }
+
+ case Calendar.Intent.NextMeeting:
+ {
+ await dc.BeginDialogAsync(nameof(NextMeetingDialog), skillOptions);
+ break;
+ }
+
+ case Calendar.Intent.ChangeCalendarEntry:
+ {
+ await dc.BeginDialogAsync(nameof(UpdateEventDialog), skillOptions);
+ break;
+ }
+
+ case Calendar.Intent.FindCalendarEntry:
+ case Calendar.Intent.ShowNext:
+ case Calendar.Intent.ShowPrevious:
+ case Calendar.Intent.Summary:
+ {
+ await dc.BeginDialogAsync(nameof(SummaryDialog), skillOptions);
+ break;
+ }
+
+ case Calendar.Intent.None:
+ {
+ await dc.Context.SendActivityAsync(dc.Context.Activity.CreateReply(CalendarSharedResponses.DidntUnderstandMessage));
+ if (_skillMode)
+ {
+ await CompleteAsync(dc);
+ }
+
+ break;
+ }
+
+ default:
+ {
+ await dc.Context.SendActivityAsync(dc.Context.Activity.CreateReply(CalendarMainResponses.FeatureNotAvailable));
+
+ if (_skillMode)
+ {
+ await CompleteAsync(dc);
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ protected override async Task CompleteAsync(DialogContext dc, DialogTurnResult result = null, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (_skillMode)
+ {
+ var response = dc.Context.Activity.CreateReply();
+ response.Type = ActivityTypes.EndOfConversation;
+
+ await dc.Context.SendActivityAsync(response);
+ }
+ else
+ {
+ await dc.Context.SendActivityAsync(dc.Context.Activity.CreateReply(CalendarSharedResponses.ActionEnded));
+ }
+
+ // End active dialog
+ await dc.EndDialogAsync(result);
+ }
+
+ protected override async Task OnEventAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (dc.Context.Activity.Name == "tokens/response")
+ {
+ // Auth dialog completion
+ var result = await dc.ContinueDialogAsync();
+
+ // If the dialog completed when we sent the token, end the skill conversation
+ if (result.Status != DialogTurnStatus.Waiting)
+ {
+ var response = dc.Context.Activity.CreateReply();
+ response.Type = ActivityTypes.EndOfConversation;
+
+ await dc.Context.SendActivityAsync(response);
+ }
+ }
+ }
+
+ protected override async Task OnInterruptDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var result = InterruptionAction.NoAction;
+
+ if (dc.Context.Activity.Type == ActivityTypes.Message)
+ {
+ // Update state with email luis result and entities
+ var calendarLuisResult = await _services.LuisServices["calendar"].RecognizeAsync(dc.Context, cancellationToken);
+ var state = await _stateAccessor.GetAsync(dc.Context, () => new CalendarSkillState());
+ state.LuisResult = calendarLuisResult;
+
+ // check luis intent
+ _services.LuisServices.TryGetValue("general", out var luisService);
+
+ if (luisService == null)
+ {
+ throw new Exception("The specified LUIS Model could not be found in your Skill configuration.");
+ }
+ else
+ {
+ var luisResult = await luisService.RecognizeAsync(dc.Context, cancellationToken);
+ var topIntent = luisResult.TopIntent().intent;
+
+ // check intent
+ switch (topIntent)
+ {
+ case General.Intent.Cancel:
+ {
+ result = await OnCancel(dc);
+ break;
+ }
+
+ case General.Intent.Help:
+ {
+ result = await OnHelp(dc);
+ break;
+ }
+
+ case General.Intent.Logout:
+ {
+ result = await OnLogout(dc);
+ break;
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private async Task OnCancel(DialogContext dc)
+ {
+ await dc.BeginDialogAsync(nameof(CancelDialog));
+ return InterruptionAction.StartedDialog;
+ }
+
+ private async Task OnHelp(DialogContext dc)
+ {
+ await dc.Context.SendActivityAsync(dc.Context.Activity.CreateReply(CalendarMainResponses.HelpMessage));
+ return InterruptionAction.MessageSentToUser;
+ }
+
+ private async Task OnLogout(DialogContext dc)
+ {
+ BotFrameworkAdapter adapter;
+ var supported = dc.Context.Adapter is BotFrameworkAdapter;
+ if (!supported)
+ {
+ throw new InvalidOperationException("OAuthPrompt.SignOutUser(): not supported by the current adapter");
+ }
+ else
+ {
+ adapter = (BotFrameworkAdapter)dc.Context.Adapter;
+ }
+
+ await dc.CancelAllDialogsAsync();
+
+ // Sign out user
+ await adapter.SignOutUserAsync(dc.Context, _services.AuthConnectionName);
+ await dc.Context.SendActivityAsync(dc.Context.Activity.CreateReply(CalendarMainResponses.LogOut));
+
+ return InterruptionAction.StartedDialog;
+ }
+
+ private void RegisterDialogs()
+ {
+ AddDialog(new CreateEventDialog(_services, _stateAccessor, _serviceManager));
+ AddDialog(new DeleteEventDialog(_services, _stateAccessor, _serviceManager));
+ AddDialog(new NextMeetingDialog(_services, _stateAccessor, _serviceManager));
+ AddDialog(new SummaryDialog(_services, _stateAccessor, _serviceManager));
+ AddDialog(new UpdateEventDialog(_services, _stateAccessor, _serviceManager));
+ AddDialog(new CancelDialog());
+ }
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Main/Resources/CalendarMainResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Main/Resources/CalendarMainResponses.cs
new file mode 100644
index 0000000000..089c6d470f
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Main/Resources/CalendarMainResponses.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace CalendarSkill.Dialogs.Main.Resources
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class CalendarMainResponses
+ {
+ private const string JsonFileName = "CalendarMainResponses.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+ public static BotResponse CalendarWelcomeMessage => GetBotResponse();
+
+ public static BotResponse HelpMessage => GetBotResponse();
+
+ public static BotResponse GreetingMessage => GetBotResponse();
+
+ public static BotResponse GoodbyeMessage => GetBotResponse();
+
+ public static BotResponse LogOut => GetBotResponse();
+
+ public static BotResponse FeatureNotAvailable => GetBotResponse();
+
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(CalendarMainResponses).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\Main\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Main/Resources/CalendarMainResponses.en.json b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Main/Resources/CalendarMainResponses.en.json
new file mode 100644
index 0000000000..5fc81ffb9d
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Main/Resources/CalendarMainResponses.en.json
@@ -0,0 +1,86 @@
+{
+ "CalendarWelcomeMessage": {
+ "replies": [
+ {
+ "text": "Welcome to Calendar Skill! I can provide you an overview of your meetings today, read out your upcoming meeting, or create a new meeting for you.",
+ "speak": "Welcome to Calendar Skill! I can provide you an overview of your meetings today, read out your upcoming meeting, or create a new meeting for you."
+ }
+ ],
+ "suggestedActions": [
+ "What are my meetings today?",
+ "What is my next meeting",
+ "I want to set up a meeting ",
+ "Can you update a meeting ",
+ "Can you cancel my event"
+ ],
+ "inputHint": "expectingInput"
+ },
+ "HelpMessage": {
+ "replies": [
+ {
+ "text": "I can provide you an overview of your meetings today, read out your upcoming meeting, or create a new meeting for you.",
+ "speak": "I can provide you an overview of your meetings today, read out your upcoming meeting, or create a new meeting for you."
+ }
+ ],
+ "suggestedActions": [
+ "What are my meetings today?",
+ "What is my next meeting",
+ "I want to set up a meeting",
+ "Can you update a meeting",
+ "Can you cancel my event"
+ ],
+ "inputHint": "expectingInput"
+ },
+ "GreetingMessage": {
+ "replies": [
+ {
+ "text": "Hi!",
+ "speak": "Hi!"
+ },
+ {
+ "text": "Hi there!",
+ "speak": "Hi there!"
+ },
+ {
+ "text": "Hello!",
+ "speak": "Hello!"
+ }
+ ],
+ "inputHint": "acceptingInput"
+ },
+ "GoodbyeMessage": {
+ "replies": [
+ {
+ "text": "Goodbye!",
+ "speak": "Goodbye!"
+ }
+ ],
+ "inputHint": "acceptingInput"
+ },
+ "LogOut": {
+ "replies": [
+ {
+ "text": "Your sign out was successful.",
+ "speak": "Your sign out was successful."
+ },
+ {
+ "text": "You have successfully signed out.",
+ "speak": "You have successfully signed out."
+ },
+ {
+ "text": "You have been logged out.",
+ "speak": "You have been logged out."
+ }
+ ],
+ "inputHint": "acceptingInput"
+ },
+ "FeatureNotAvailable": {
+ "replies": [
+ {
+ "text": "This feature is not yet available in the Calendar Skill. Please try asking something else.",
+ "speak": "This feature is not yet available in the Calendar Skill. Please try asking something else."
+ }
+ ],
+ "inputHint": "acceptingInput"
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Main/Resources/CalendarMainResponses.tt b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Main/Resources/CalendarMainResponses.tt
new file mode 100644
index 0000000000..3e815afb84
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Main/Resources/CalendarMainResponses.tt
@@ -0,0 +1,108 @@
+<#@ assembly name="Newtonsoft.Json.dll" #>
+<#@ template debug="false" hostspecific="true" language="C#" #>
+<#@ output extension=".cs" #>
+<#
+ var className = System.IO.Path.GetFileNameWithoutExtension(Host.TemplateFile);
+ var namespaceName = System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint");;
+ string myFile = System.IO.File.ReadAllText(this.Host.ResolvePath(className + ".en.json"));
+ var json = Newtonsoft.Json.JsonConvert.DeserializeObject>(myFile);
+#>
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace <#= namespaceName #>
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class <#= className #>
+ {
+ private const string JsonFileName = "<#=className#>.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+<# foreach (var propertyName in json) { #>
+ public static BotResponse <#= propertyName.Key.Substring(0, 1).ToUpperInvariant() + propertyName.Key.Substring(1) #> => GetBotResponse();
+
+<# } #>
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(<#= className #>).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\Main\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/NextMeeting/Resources/NextMeetingResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/NextMeeting/Resources/NextMeetingResponses.cs
new file mode 100644
index 0000000000..d5a4480e7a
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/NextMeeting/Resources/NextMeetingResponses.cs
@@ -0,0 +1,103 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace CalendarSkill.Dialogs.NextMeeting.Resources
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class NextMeetingResponses
+ {
+ private const string JsonFileName = "NextMeetingResponses.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+ public static BotResponse ShowNoMeetingMessage => GetBotResponse();
+
+ public static BotResponse ShowNextMeetingNoLocationMessage => GetBotResponse();
+
+ public static BotResponse ShowNextMeetingMessage => GetBotResponse();
+
+ public static BotResponse ShowMultipleNextMeetingMessage => GetBotResponse();
+
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(NextMeetingResponses).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\NextMeeting\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/NextMeeting/Resources/NextMeetingResponses.en.json b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/NextMeeting/Resources/NextMeetingResponses.en.json
new file mode 100644
index 0000000000..5bd686f9ec
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/NextMeeting/Resources/NextMeetingResponses.en.json
@@ -0,0 +1,38 @@
+{
+ "ShowNoMeetingMessage": {
+ "replies": [
+ {
+ "text": "You don't have any meetings.",
+ "speak": "You don't have any meetings."
+ }
+ ],
+ "inputHint": "acceptingInput"
+ },
+ "ShowNextMeetingNoLocationMessage": {
+ "replies": [
+ {
+ "text": "You have the following event on your calendar:",
+ "speak": "You have the following event on your calendar: You have {EventName} at {EventTime} with {PeopleCount} people."
+ }
+ ],
+ "inputHint": "acceptingInput"
+ },
+ "ShowNextMeetingMessage": {
+ "replies": [
+ {
+ "text": "You have the following event on your calendar:",
+ "speak": "You have the following event on your calendar: You have {EventName} at {EventTime} with {PeopleCount} people at {Location}."
+ }
+ ],
+ "inputHint": "acceptingInput"
+ },
+ "ShowMultipleNextMeetingMessage": {
+ "replies": [
+ {
+ "text": "You have the following event on your calendar:",
+ "speak": "You have the following event on your calendar:"
+ }
+ ],
+ "inputHint": "acceptingInput"
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/NextMeeting/Resources/NextMeetingResponses.tt b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/NextMeeting/Resources/NextMeetingResponses.tt
new file mode 100644
index 0000000000..5a4bd898c8
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/NextMeeting/Resources/NextMeetingResponses.tt
@@ -0,0 +1,108 @@
+<#@ assembly name="Newtonsoft.Json.dll" #>
+<#@ template debug="false" hostspecific="true" language="C#" #>
+<#@ output extension=".cs" #>
+<#
+ var className = System.IO.Path.GetFileNameWithoutExtension(Host.TemplateFile);
+ var namespaceName = System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint");;
+ string myFile = System.IO.File.ReadAllText(this.Host.ResolvePath(className + ".en.json"));
+ var json = Newtonsoft.Json.JsonConvert.DeserializeObject>(myFile);
+#>
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace <#= namespaceName #>
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class <#= className #>
+ {
+ private const string JsonFileName = "<#=className#>.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+<# foreach (var propertyName in json) { #>
+ public static BotResponse <#= propertyName.Key.Substring(0, 1).ToUpperInvariant() + propertyName.Key.Substring(1) #> => GetBotResponse();
+
+<# } #>
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(<#= className #>).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\NextMeeting\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Actions.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Actions.cs
new file mode 100644
index 0000000000..07be42604a
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Actions.cs
@@ -0,0 +1,28 @@
+namespace CalendarSkill
+{
+ public static class Actions
+ {
+ public const string Login = "login";
+ public const string Prompt = "prompt";
+ public const string Choice = "choice";
+ public const string EventChoice = "event_choice";
+ public const string ShowEventsSummary = "showEventsSummary";
+ public const string ShowNextEvent = "showNextEvent";
+ public const string CreateEvent = "createEvent";
+ public const string UpdateEventTime = "UpdateEventTime";
+ public const string DeleteEvent = "DeleteEvent";
+ public const string UpdateAddress = "UpdateAddress";
+ public const string UpdateName = "UpdateName";
+ public const string ConfirmAttendee = "ConfirmAttendee";
+ public const string TakeFurtherAction = "TakeFurtherAction";
+ public const string UpdateStartTime = "UpdateStartTime";
+ public const string UpdateNewStartTime = "UpdateNewStartTime";
+ public const string UpdateStartDateForCreate = "UpdateStartDateForCreate";
+ public const string UpdateStartTimeForCreate = "UpdateStartTimeForCreate";
+ public const string UpdateDurationForCreate = "UpdateDurationForCreate";
+ public const string DateTimePrompt = "DateTimePrompt";
+ public const string DateTimePromptForUpdateDelete = "DateTimePromptForUpdateDelete";
+ public const string Read = "read";
+ public const string Greeting = "greeting";
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/CalendarSkillResponseBuilder.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/CalendarSkillResponseBuilder.cs
new file mode 100644
index 0000000000..aee911a861
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/CalendarSkillResponseBuilder.cs
@@ -0,0 +1,14 @@
+using Microsoft.Bot.Solutions.Dialogs;
+using Microsoft.Bot.Solutions.Dialogs.BotResponseFormatters;
+
+namespace CalendarSkill
+{
+ public class CalendarSkillResponseBuilder : BotResponseBuilder
+ {
+ public CalendarSkillResponseBuilder()
+ : base()
+ {
+ AddFormatter(new TextBotResponseFormatter());
+ }
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/CalendarSkillDialogOptions.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/DialogOptions/CalendarSkillDialogOptions.cs
similarity index 98%
rename from solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/CalendarSkillDialogOptions.cs
rename to solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/DialogOptions/CalendarSkillDialogOptions.cs
index fedfbb7f8e..eb21ba71a4 100644
--- a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/CalendarSkillDialogOptions.cs
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/DialogOptions/CalendarSkillDialogOptions.cs
@@ -4,4 +4,4 @@ public class CalendarSkillDialogOptions
{
public bool SkillMode { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/UpdateAddressDialogOptions.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/DialogOptions/UpdateAddressDialogOptions.cs
similarity index 89%
rename from solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/UpdateAddressDialogOptions.cs
rename to solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/DialogOptions/UpdateAddressDialogOptions.cs
index bf55046f80..2a81a08088 100644
--- a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/UpdateAddressDialogOptions.cs
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/DialogOptions/UpdateAddressDialogOptions.cs
@@ -7,12 +7,12 @@ public class UpdateAddressDialogOptions
{
public UpdateAddressDialogOptions()
{
- this.Reason = UpdateReason.NotFound;
+ Reason = UpdateReason.NotFound;
}
public UpdateAddressDialogOptions(UpdateReason reason)
{
- this.Reason = reason;
+ Reason = reason;
}
public enum UpdateReason
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/UpdateDateTimeDialogOptions.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/DialogOptions/UpdateDateTimeDialogOptions.cs
similarity index 90%
rename from solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/UpdateDateTimeDialogOptions.cs
rename to solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/DialogOptions/UpdateDateTimeDialogOptions.cs
index 381c8e7d64..bcee89a88a 100644
--- a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/UpdateDateTimeDialogOptions.cs
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/DialogOptions/UpdateDateTimeDialogOptions.cs
@@ -7,12 +7,12 @@ public class UpdateDateTimeDialogOptions
{
public UpdateDateTimeDialogOptions()
{
- this.Reason = UpdateReason.NotFound;
+ Reason = UpdateReason.NotFound;
}
public UpdateDateTimeDialogOptions(UpdateReason reason)
{
- this.Reason = reason;
+ Reason = reason;
}
public enum UpdateReason
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/UpdateUserNameDialogOptions.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/DialogOptions/UpdateUserNameDialogOptions.cs
similarity index 89%
rename from solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/UpdateUserNameDialogOptions.cs
rename to solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/DialogOptions/UpdateUserNameDialogOptions.cs
index 9e75e9ab57..445e7b3ed6 100644
--- a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/UpdateUserNameDialogOptions.cs
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/DialogOptions/UpdateUserNameDialogOptions.cs
@@ -7,12 +7,12 @@ public class UpdateUserNameDialogOptions
{
public UpdateUserNameDialogOptions()
{
- this.Reason = UpdateReason.NotFound;
+ Reason = UpdateReason.NotFound;
}
public UpdateUserNameDialogOptions(UpdateReason reason)
{
- this.Reason = reason;
+ Reason = reason;
}
public enum UpdateReason
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Resources/CalendarSharedResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Resources/CalendarSharedResponses.cs
new file mode 100644
index 0000000000..3f834f3082
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Resources/CalendarSharedResponses.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace CalendarSkill.Dialogs.Shared.Resources
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class CalendarSharedResponses
+ {
+ private const string JsonFileName = "CalendarSharedResponses.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+ public static BotResponse DidntUnderstandMessage => GetBotResponse();
+
+ public static BotResponse CancellingMessage => GetBotResponse();
+
+ public static BotResponse NoAuth => GetBotResponse();
+
+ public static BotResponse AuthFailed => GetBotResponse();
+
+ public static BotResponse ActionEnded => GetBotResponse();
+
+ public static BotResponse CalendarErrorMessage => GetBotResponse();
+
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(CalendarSharedResponses).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\Shared\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Resources/CalendarSharedResponses.en.json b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Resources/CalendarSharedResponses.en.json
new file mode 100644
index 0000000000..fddef29b61
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Resources/CalendarSharedResponses.en.json
@@ -0,0 +1,148 @@
+{
+ "DidntUnderstandMessage": {
+ "replies": [
+ {
+ "text": "Sorry, I didn't understand what you meant.",
+ "speak": "Sorry, I didn't understand what you meant."
+ },
+ {
+ "text": "I didn't understand, perhaps try again in a different way.",
+ "speak": "I didn't understand, perhaps try again in a different way."
+ },
+ {
+ "text": "Can you try to ask in a different way?",
+ "speak": "Can you try to ask in a different way?"
+ },
+ {
+ "text": "I didn't get what you mean, can you try in a different way?",
+ "speak": "I didn't get what you mean, can you try in a different way?"
+ },
+ {
+ "text": "Could you elaborate?",
+ "speak": "Could you elaborate?"
+ },
+ {
+ "text": "Please say that again in a different way.",
+ "speak": "Please say that again in a different way."
+ },
+ {
+ "text": "I didn't quite get that.",
+ "speak": "I didn't quite get that."
+ },
+ {
+ "text": "Can you say that in a different way?",
+ "speak": "Can you say that in a different way?"
+ },
+ {
+ "text": "Can you try to ask me again? I didn't get what you mean.",
+ "speak": "Can you try to ask me again? I didn't get what you mean."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "CancellingMessage": {
+ "replies": [
+ {
+ "text": "Sure, we can do this later.",
+ "speak": "Sure, we can do this later."
+ },
+ {
+ "text": "Sure, we can start this later.",
+ "speak": "Sure, we can start this later."
+ },
+ {
+ "text": "No problem, you can try again at another time.",
+ "speak": "No problem, you can try again at another time."
+ },
+ {
+ "text": "Alright, let me know when you need my help.",
+ "speak": "Alright, let me know when you need my help."
+ },
+ {
+ "text": "Sure, I'm here if you need me.",
+ "speak": "Sure, I'm here if you need me."
+ }
+ ],
+ "inputHint": "acceptingInput"
+ },
+ "NoAuth": {
+ "replies": [
+ {
+ "text": "Please log in before taking further action.",
+ "speak": "Please log in before taking further action."
+ },
+ {
+ "text": "Please log in so I can take further action.",
+ "speak": "Please log in so I can take further action."
+ },
+ {
+ "text": "Please log in so I can proceed with your request.",
+ "speak": "Please log in so I can proceed with your request."
+ },
+ {
+ "text": "Can you log in so I can help you out further?",
+ "speak": "Can you log in so I can help you out further?"
+ },
+ {
+ "text": "You need to log in so I can take further action.",
+ "speak": "You need to log in so I can take further action."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "AuthFailed": {
+ "replies": [
+ {
+ "text": "Authentication failed. Please try again",
+ "speak": "Authentication failed. Please try again."
+ },
+ {
+ "text": "You failed to log in. Please try again later.",
+ "speak": "You failed to log in. please try again later."
+ },
+ {
+ "text": "Your log in failed. Let's try this again.",
+ "speak": "Your log in failed. Let's try this again."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "ActionEnded": {
+ "replies": [
+ {
+ "text": "Let me know if you need my help with something else.",
+ "speak": "Let me know if you need my help with something else."
+ },
+ {
+ "text": "I'm here if you need me.",
+ "speak": "I'm here if you need me."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "CalendarErrorMessage": {
+ "replies": [
+ {
+ "text": "Sorry, it looks like something went wrong!",
+ "speak": "Sorry, it looks like something went wrong!"
+ },
+ {
+ "text": "An error occurred, please try again later.",
+ "speak": "An error occurred, please try again later."
+ },
+ {
+ "text": "Something went wrong, sorry!",
+ "speak": "Something went wrong, sorry!"
+ },
+ {
+ "text": "It seems like something went wrong. Can you try again later?",
+ "speak": "It seems like something went wrong. Can you try again later?"
+ },
+ {
+ "text": "Sorry I can't help right now. Please try again later.",
+ "speak": "Sorry I can't help right now. Please try again later."
+ }
+ ],
+ "inputHint": "expectingInput"
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/Resources/CalendarBotResponses.tt b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Resources/CalendarSharedResponses.tt
similarity index 100%
rename from solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/Resources/CalendarBotResponses.tt
rename to solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Resources/CalendarSharedResponses.tt
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/Resources/CalendarCardData.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Resources/Cards/CalendarCardData.cs
similarity index 66%
rename from solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/Resources/CalendarCardData.cs
rename to solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Resources/Cards/CalendarCardData.cs
index bcdc66caaf..663a934ae4 100644
--- a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/Resources/CalendarCardData.cs
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Shared/Resources/Cards/CalendarCardData.cs
@@ -1,7 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT license.
-
-using Microsoft.Bot.Solutions.Cards;
+using Microsoft.Bot.Solutions.Cards;
namespace CalendarSkill
{
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Summary/Resources/SummaryResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Summary/Resources/SummaryResponses.cs
new file mode 100644
index 0000000000..8ab6a19d21
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Summary/Resources/SummaryResponses.cs
@@ -0,0 +1,109 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace CalendarSkill.Dialogs.Summary.Resources
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class SummaryResponses
+ {
+ private const string JsonFileName = "SummaryResponses.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+ public static BotResponse CalendarNoMoreEvent => GetBotResponse();
+
+ public static BotResponse CalendarNoPreviousEvent => GetBotResponse();
+
+ public static BotResponse ShowNoMeetingMessage => GetBotResponse();
+
+ public static BotResponse ShowOneMeetingSummaryMessage => GetBotResponse();
+
+ public static BotResponse ReadOutPrompt => GetBotResponse();
+
+ public static BotResponse ReadOutMorePrompt => GetBotResponse();
+
+ public static BotResponse ReadOutMessage => GetBotResponse();
+
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(SummaryResponses).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\Summary\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Summary/Resources/SummaryResponses.en.json b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Summary/Resources/SummaryResponses.en.json
new file mode 100644
index 0000000000..04ea60fc9e
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Summary/Resources/SummaryResponses.en.json
@@ -0,0 +1,101 @@
+{
+ "CalendarNoMoreEvent": {
+ "replies": [
+ {
+ "text": "No more meetings!",
+ "speak": "No more meetings!"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "CalendarNoPreviousEvent": {
+ "replies": [
+ {
+ "text": "This is already the first page!",
+ "speak": "This is already the first page!"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "ShowNoMeetingMessage": {
+ "replies": [
+ {
+ "text": "You don't have any meetings.",
+ "speak": "You don't have any meetings."
+ }
+ ],
+ "inputHint": "ignoringInput"
+ },
+ "ShowOneMeetingSummaryMessage": {
+ "replies": [
+ {
+ "text": "I found {Count} events for you today:",
+ "speak": "I found {Count} events for you today: It is {EventName1} for {EventDuration}."
+ }
+ ],
+ "inputHint": "ignoringInput"
+ },
+ "ReadOutPrompt": {
+ "replies": [
+ {
+ "text": "Which one do you want to hear?",
+ "speak": "Which one do you want to hear?"
+ },
+ {
+ "text": "Which meeting would you like me to read out?",
+ "speak": "Which meeting would you like me to read out?"
+ },
+ {
+ "text": "Any meeting you would like me to read out?",
+ "speak": "Any meeting you would like me to read out?"
+ },
+ {
+ "text": "Which meeting do you want more details on?",
+ "speak": "Which meeting do you want more details on?"
+ },
+ {
+ "text": "Which one would you like to know the content of?",
+ "speak": "Which one would you like to know the content of?"
+ },
+ {
+ "text": "Which meeting do you want me to read out?",
+ "speak": "Which meeting do you want me to read out?"
+ },
+ {
+ "text": "Which one do you want to hear about?",
+ "speak": "Which one do you want to hear about?"
+ },
+ {
+ "text": "Which meeting do you want me to give you the details?",
+ "speak": "Which meeting do you want me to give you the details?"
+ },
+ {
+ "text": "Which meeting should I read to you?",
+ "speak": "Which meeting should I read to you?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "ReadOutMorePrompt": {
+ "replies": [
+ {
+ "text": "Do you want to hear more? Which one do you want to hear?",
+ "speak": "Do you want to hear more? Which one do you want to hear?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "ReadOutMessage": {
+ "replies": [
+ {
+ "text": "Details of meeting:",
+ "speak": "Details of event"
+ },
+ {
+ "text": "Content of meeting:",
+ "speak": "Content of event:"
+ }
+ ],
+ "inputHint": "expectingInput"
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Summary/Resources/SummaryResponses.tt b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Summary/Resources/SummaryResponses.tt
new file mode 100644
index 0000000000..bfac6a4a68
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/Summary/Resources/SummaryResponses.tt
@@ -0,0 +1,108 @@
+<#@ assembly name="Newtonsoft.Json.dll" #>
+<#@ template debug="false" hostspecific="true" language="C#" #>
+<#@ output extension=".cs" #>
+<#
+ var className = System.IO.Path.GetFileNameWithoutExtension(Host.TemplateFile);
+ var namespaceName = System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint");;
+ string myFile = System.IO.File.ReadAllText(this.Host.ResolvePath(className + ".en.json"));
+ var json = Newtonsoft.Json.JsonConvert.DeserializeObject>(myFile);
+#>
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace <#= namespaceName #>
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class <#= className #>
+ {
+ private const string JsonFileName = "<#=className#>.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+<# foreach (var propertyName in json) { #>
+ public static BotResponse <#= propertyName.Key.Substring(0, 1).ToUpperInvariant() + propertyName.Key.Substring(1) #> => GetBotResponse();
+
+<# } #>
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(<#= className #>).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\Summary\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/UpdateEvent/Resources/UpdateEventResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/UpdateEvent/Resources/UpdateEventResponses.cs
new file mode 100644
index 0000000000..a50fd1afcd
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/UpdateEvent/Resources/UpdateEventResponses.cs
@@ -0,0 +1,113 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace CalendarSkill.Dialogs.UpdateEvent.Resources
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class UpdateEventResponses
+ {
+ private const string JsonFileName = "UpdateEventResponses.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+ public static BotResponse NotEventOrganizer => GetBotResponse();
+
+ public static BotResponse ConfirmUpdate => GetBotResponse();
+
+ public static BotResponse ConfirmUpdateFailed => GetBotResponse();
+
+ public static BotResponse EventUpdated => GetBotResponse();
+
+ public static BotResponse NoNewTime => GetBotResponse();
+
+ public static BotResponse EventWithStartTimeNotFound => GetBotResponse();
+
+ public static BotResponse NoDeleteStartTime => GetBotResponse();
+
+ public static BotResponse NoUpdateStartTime => GetBotResponse();
+
+ public static BotResponse MultipleEventsStartAtSameTime => GetBotResponse();
+
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(UpdateEventResponses).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\UpdateEvent\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/UpdateEvent/Resources/UpdateEventResponses.en.json b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/UpdateEvent/Resources/UpdateEventResponses.en.json
new file mode 100644
index 0000000000..c95052faa4
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/UpdateEvent/Resources/UpdateEventResponses.en.json
@@ -0,0 +1,113 @@
+{
+ "NotEventOrganizer": {
+ "replies": [
+ {
+ "text": "I can't update this event because you are not the organizer.",
+ "speak": "I can't update this event because you are not the organizer."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "ConfirmUpdate": {
+ "replies": [
+ {
+ "text": "Are you sure you want to update the following?",
+ "speak": "Are you sure you want to update the following?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "ConfirmUpdateFailed": {
+ "replies": [
+ {
+ "text": "Are you sure you want to update the following? Please confirm.",
+ "speak": "Are you sure you want to update the following? Please confirm."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "EventUpdated": {
+ "replies": [
+
+ {
+ "text": "I have changed the time of your event.",
+ "speak": "I have changed the time of your event."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoNewTime": {
+ "replies": [
+
+ {
+ "text": "What will be the new time of the event?",
+ "speak": "What will be the new time of the event?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "EventWithStartTimeNotFound": {
+ "replies": [
+
+ {
+ "text": "No event start at this time or with that title, please try again.",
+ "speak": "No event start at this time or with that title, please try again."
+ },
+ {
+ "text": "I could not find an event you mentioned, please try again.",
+ "speak": "I could not find an event you mentioned, please try again."
+ },
+ {
+ "text": "No event can be found with the event time or title, can you try again?",
+ "speak": "No event can be found with the event time or title, can you try again?"
+ },
+ {
+ "text": "It seems there is no event with that name, try again please.",
+ "speak": "It seems there is no event with that name, try again please."
+ },
+ {
+ "text": "It seems there is no event at this time, please try again",
+ "speak": "It seems there is no event at this time, please try again"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoDeleteStartTime": {
+ "replies": [
+
+ {
+ "text": "What event would you like to delete? By communicating the start time or title I can retrieve your event.",
+ "speak": "What event would you like to delete? By communicating the start time or title I can retrieve your event."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoUpdateStartTime": {
+ "replies": [
+
+ {
+ "text": "What event would you like to adjust? By communicating the start time or title I can retrieve your event.",
+ "speak": "What event would you like to adjust? By communicating the start time or title I can retrieve your event."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "MultipleEventsStartAtSameTime": {
+ "replies": [
+
+ {
+ "text": "Multiple events found, please select the one you need.",
+ "speak": "Multiple events found, please select the one you need."
+ },
+ {
+ "text": "I have found multiple events, can you select the event of choice.",
+ "speak": "I have found multiple events, can you select the event of choice."
+ },
+ {
+ "text": "There are multiple events, please select the relevant event.",
+ "speak": "There are multiple events, please select the relevant event."
+ }
+ ],
+ "inputHint": "expectingInput"
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/UpdateEvent/Resources/UpdateEventResponses.tt b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/UpdateEvent/Resources/UpdateEventResponses.tt
new file mode 100644
index 0000000000..006673ab81
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/UpdateEvent/Resources/UpdateEventResponses.tt
@@ -0,0 +1,108 @@
+<#@ assembly name="Newtonsoft.Json.dll" #>
+<#@ template debug="false" hostspecific="true" language="C#" #>
+<#@ output extension=".cs" #>
+<#
+ var className = System.IO.Path.GetFileNameWithoutExtension(Host.TemplateFile);
+ var namespaceName = System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint");;
+ string myFile = System.IO.File.ReadAllText(this.Host.ResolvePath(className + ".en.json"));
+ var json = Newtonsoft.Json.JsonConvert.DeserializeObject>(myFile);
+#>
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace <#= namespaceName #>
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class <#= className #>
+ {
+ private const string JsonFileName = "<#=className#>.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+<# foreach (var propertyName in json) { #>
+ public static BotResponse <#= propertyName.Key.Substring(0, 1).ToUpperInvariant() + propertyName.Key.Substring(1) #> => GetBotResponse();
+
+<# } #>
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(<#= className #>).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\UpdateEvent\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/UpdateEvent/UpdateEventDialog.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/UpdateEvent/UpdateEventDialog.cs
new file mode 100644
index 0000000000..7099a599c4
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/Dialogs/UpdateEvent/UpdateEventDialog.cs
@@ -0,0 +1,376 @@
+using CalendarSkill.Dialogs.Main.Resources;
+using CalendarSkill.Dialogs.Shared.Resources;
+using CalendarSkill.Dialogs.UpdateEvent.Resources;
+using Microsoft.Bot.Builder;
+using Microsoft.Bot.Builder.Dialogs;
+using Microsoft.Bot.Builder.Dialogs.Choices;
+using Microsoft.Bot.Schema;
+using Microsoft.Bot.Solutions.Extensions;
+using Microsoft.Bot.Solutions.Skills;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace CalendarSkill
+{
+ public class UpdateEventDialog : CalendarSkillDialog
+ {
+ public UpdateEventDialog(
+ SkillConfiguration services,
+ IStatePropertyAccessor accessor,
+ IServiceManager serviceManager)
+ : base(nameof(UpdateEventDialog), services, accessor, serviceManager)
+ {
+ var updateEvent = new WaterfallStep[]
+ {
+ GetAuthToken,
+ AfterGetAuthToken,
+ FromTokenToStartTime,
+ FromEventsToNewDate,
+ ConfirmBeforeUpdate,
+ UpdateEventTime,
+ };
+
+ var updateStartTime = new WaterfallStep[]
+ {
+ UpdateStartTime,
+ AfterUpdateStartTime,
+ };
+
+ var updateNewStartTime = new WaterfallStep[]
+ {
+ GetNewEventTime,
+ AfterGetNewEventTime,
+ };
+
+ // Define the conversation flow using a waterfall model.
+ AddDialog(new WaterfallDialog(Actions.UpdateEventTime, updateEvent));
+ AddDialog(new WaterfallDialog(Actions.UpdateStartTime, updateStartTime));
+ AddDialog(new WaterfallDialog(Actions.UpdateNewStartTime, updateNewStartTime));
+
+ // Set starting dialog for component
+ InitialDialogId = Actions.UpdateEventTime;
+ }
+
+ public async Task FromEventsToNewDate(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ try
+ {
+ var state = await _accessor.GetAsync(sc.Context);
+ if (sc.Result != null && state.Events.Count > 1)
+ {
+ var events = state.Events;
+ state.Events = new List
+ {
+ events[(sc.Result as FoundChoice).Index],
+ };
+ }
+
+ var origin = state.Events[0];
+ if (!origin.IsOrganizer)
+ {
+ await sc.Context.SendActivityAsync(sc.Context.Activity.CreateReply(UpdateEventResponses.NotEventOrganizer));
+ state.Clear();
+ return await sc.EndDialogAsync(true);
+ }
+ else if (state.NewStartDateTime == null)
+ {
+ return await sc.BeginDialogAsync(Actions.UpdateNewStartTime, new UpdateDateTimeDialogOptions(UpdateDateTimeDialogOptions.UpdateReason.NotFound));
+ }
+ else
+ {
+ return await sc.NextAsync();
+ }
+ }
+ catch
+ {
+ await HandleDialogExceptions(sc);
+ throw;
+ }
+ }
+
+ public async Task ConfirmBeforeUpdate(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ try
+ {
+ var state = await _accessor.GetAsync(sc.Context);
+
+ var newStartTime = (DateTime)state.NewStartDateTime;
+ newStartTime = DateTime.SpecifyKind(newStartTime, DateTimeKind.Local);
+
+ // DateTime newStartTime = DateTime.Parse((string)state.NewStartDateTime);
+ var origin = state.Events[0];
+ var last = origin.EndTime - origin.StartTime;
+ origin.StartTime = newStartTime;
+ origin.EndTime = (newStartTime + last).AddSeconds(1);
+ var replyMessage = sc.Context.Activity.CreateAdaptiveCardReply(UpdateEventResponses.ConfirmUpdate, origin.OnlineMeetingUrl == null ? "Dialogs/Shared/Resources/Cards/CalendarCardNoJoinButton.json" : "Dialogs/Shared/Resources/Cards/CalendarCard.json", origin.ToAdaptiveCardData());
+
+ return await sc.PromptAsync(Actions.TakeFurtherAction, new PromptOptions
+ {
+ Prompt = replyMessage,
+ RetryPrompt = sc.Context.Activity.CreateReply(UpdateEventResponses.ConfirmUpdateFailed, _responseBuilder),
+ });
+ }
+ catch
+ {
+ await HandleDialogExceptions(sc);
+ throw;
+ }
+ }
+
+ public async Task UpdateEventTime(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ try
+ {
+ var confirmResult = (bool)sc.Result;
+ if (confirmResult)
+ {
+ var state = await _accessor.GetAsync(sc.Context);
+
+ var newStartTime = (DateTime)state.NewStartDateTime;
+
+ var origin = state.Events[0];
+ var updateEvent = new EventModel(origin.Source);
+ var last = origin.EndTime - origin.StartTime;
+ updateEvent.StartTime = TimeZoneInfo.ConvertTimeToUtc(newStartTime, state.GetUserTimeZone());
+ updateEvent.EndTime = TimeZoneInfo.ConvertTimeToUtc((newStartTime + last).AddSeconds(1), state.GetUserTimeZone());
+ updateEvent.TimeZone = TimeZoneInfo.Utc;
+ updateEvent.Id = origin.Id;
+ var calendarService = _serviceManager.InitCalendarService(state.APIToken, state.EventSource, state.GetUserTimeZone());
+ var newEvent = await calendarService.UpdateEventById(updateEvent);
+
+ newEvent.StartTime = TimeZoneInfo.ConvertTimeFromUtc(newEvent.StartTime, state.GetUserTimeZone());
+ newEvent.EndTime = TimeZoneInfo.ConvertTimeFromUtc(newEvent.EndTime, state.GetUserTimeZone());
+ var replyMessage = sc.Context.Activity.CreateAdaptiveCardReply(UpdateEventResponses.EventUpdated, newEvent.OnlineMeetingUrl == null ? "Dialogs/Shared/Resources/Cards/CalendarCardNoJoinButton.json" : "Dialogs/Shared/Resources/Cards/CalendarCard.json", newEvent.ToAdaptiveCardData());
+ await sc.Context.SendActivityAsync(replyMessage);
+ state.Clear();
+ }
+ else
+ {
+ await sc.Context.SendActivityAsync(sc.Context.Activity.CreateReply(CalendarSharedResponses.ActionEnded));
+ }
+
+ return await sc.EndDialogAsync(true);
+ }
+ catch
+ {
+ await HandleDialogExceptions(sc);
+ throw;
+ }
+ }
+
+ public async Task GetNewEventTime(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ try
+ {
+ if (((UpdateDateTimeDialogOptions)sc.Options).Reason == UpdateDateTimeDialogOptions.UpdateReason.NotFound)
+ {
+ return await sc.PromptAsync(Actions.DateTimePrompt, new PromptOptions { Prompt = sc.Context.Activity.CreateReply(UpdateEventResponses.NoNewTime) });
+ }
+ else
+ {
+ return await sc.PromptAsync(Actions.DateTimePrompt, new PromptOptions { Prompt = sc.Context.Activity.CreateReply(CalendarSharedResponses.DidntUnderstandMessage) });
+ }
+ }
+ catch
+ {
+ await HandleDialogExceptions(sc);
+ throw;
+ }
+ }
+
+ public async Task AfterGetNewEventTime(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ try
+ {
+ var state = await _accessor.GetAsync(sc.Context);
+ if (sc.Result != null)
+ {
+ IList dateTimeResolutions = sc.Result as List;
+ var newStartTime = DateTime.Parse(dateTimeResolutions.First().Value);
+ var dateTimeConvertType = dateTimeResolutions.First().Timex;
+
+ if (newStartTime != null)
+ {
+ var isRelativeTime = IsRelativeTime(sc.Context.Activity.Text, dateTimeResolutions.First().Value, dateTimeResolutions.First().Timex);
+ state.NewStartDateTime = isRelativeTime ? TimeZoneInfo.ConvertTime(newStartTime, TimeZoneInfo.Local, state.GetUserTimeZone()) : newStartTime;
+ return await sc.ContinueDialogAsync();
+ }
+ else
+ {
+ return await sc.BeginDialogAsync(Actions.UpdateNewStartTime, new UpdateDateTimeDialogOptions(UpdateDateTimeDialogOptions.UpdateReason.NotADateTime));
+ }
+ }
+ else
+ {
+ return await sc.BeginDialogAsync(Actions.UpdateNewStartTime, new UpdateDateTimeDialogOptions(UpdateDateTimeDialogOptions.UpdateReason.NotADateTime));
+ }
+ }
+ catch
+ {
+ await HandleDialogExceptions(sc);
+ throw;
+ }
+ }
+
+ public async Task FromTokenToStartTime(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ try
+ {
+ var state = await _accessor.GetAsync(sc.Context);
+ if (string.IsNullOrEmpty(state.APIToken))
+ {
+ return await sc.EndDialogAsync(true);
+ }
+
+ var calendarService = _serviceManager.InitCalendarService(state.APIToken, state.EventSource, state.GetUserTimeZone());
+
+ if (state.StartDateTime == null)
+ {
+ return await sc.BeginDialogAsync(Actions.UpdateStartTime, new UpdateDateTimeDialogOptions(UpdateDateTimeDialogOptions.UpdateReason.NotFound));
+ }
+ else
+ {
+ return await sc.NextAsync();
+ }
+ }
+ catch
+ {
+ await HandleDialogExceptions(sc);
+ throw;
+ }
+ }
+
+ public async Task UpdateStartTime(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ try
+ {
+ var state = await _accessor.GetAsync(sc.Context);
+
+ if (((UpdateDateTimeDialogOptions)sc.Options).Reason == UpdateDateTimeDialogOptions.UpdateReason.NoEvent)
+ {
+ return await sc.PromptAsync(Actions.DateTimePromptForUpdateDelete, new PromptOptions
+ {
+ Prompt = sc.Context.Activity.CreateReply(UpdateEventResponses.EventWithStartTimeNotFound),
+ });
+ }
+ else
+ {
+ if (state.DialogName == "DeleteEvent")
+ {
+ return await sc.PromptAsync(Actions.DateTimePromptForUpdateDelete, new PromptOptions
+ {
+ Prompt = sc.Context.Activity.CreateReply(UpdateEventResponses.NoDeleteStartTime),
+ });
+ }
+ else
+ {
+ return await sc.PromptAsync(Actions.DateTimePromptForUpdateDelete, new PromptOptions
+ {
+ Prompt = sc.Context.Activity.CreateReply(UpdateEventResponses.NoUpdateStartTime),
+ });
+ }
+ }
+ }
+ catch
+ {
+ await HandleDialogExceptions(sc);
+ throw;
+ }
+ }
+
+ public async Task AfterUpdateStartTime(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ try
+ {
+ var state = await _accessor.GetAsync(sc.Context);
+ sc.Context.Activity.Properties.TryGetValue("OriginText", out var content);
+ var userInput = content != null ? content.ToString() : sc.Context.Activity.Text;
+ DateTime? startTime = null;
+ try
+ {
+ IList dateTimeResolutions = sc.Result as List;
+ if (dateTimeResolutions.Count > 0)
+ {
+ startTime = DateTime.Parse(dateTimeResolutions.First().Value);
+ var dateTimeConvertType = dateTimeResolutions.First().Timex;
+ bool isRelativeTime = IsRelativeTime(sc.Context.Activity.Text, dateTimeResolutions.First().Value, dateTimeResolutions.First().Timex);
+ startTime = isRelativeTime ? TimeZoneInfo.ConvertTime(startTime.Value, TimeZoneInfo.Local, state.GetUserTimeZone()) : startTime;
+ }
+ }
+ catch
+ {
+ }
+
+ var calendarService = _serviceManager.InitCalendarService(state.APIToken, state.EventSource, state.GetUserTimeZone());
+
+ var events = new List();
+ if (startTime != null)
+ {
+ state.StartDateTime = startTime;
+ startTime = DateTime.SpecifyKind(startTime.Value, DateTimeKind.Local);
+ events = await calendarService.GetEventsByStartTime(startTime.Value);
+ }
+ else
+ {
+ state.Title = userInput;
+ events = await calendarService.GetEventsByTitle(userInput);
+ }
+
+ state.Events = events;
+
+ if (events.Count <= 0)
+ {
+ return await sc.BeginDialogAsync(Actions.UpdateStartTime, new UpdateDateTimeDialogOptions(UpdateDateTimeDialogOptions.UpdateReason.NoEvent));
+ }
+ else if (events.Count > 1)
+ {
+ var options = new PromptOptions()
+ {
+ Choices = new List(),
+ };
+
+ for (var i = 0; i < events.Count; i++)
+ {
+ var item = events[i];
+ var choice = new Choice()
+ {
+ Value = string.Empty,
+ Synonyms = new List { (i + 1).ToString(), item.Title },
+ };
+ options.Choices.Add(choice);
+ }
+
+ var replyToConversation = sc.Context.Activity.CreateReply(UpdateEventResponses.MultipleEventsStartAtSameTime);
+ replyToConversation.AttachmentLayout = AttachmentLayoutTypes.Carousel;
+ replyToConversation.Attachments = new List();
+
+ var cardsData = new List();
+ foreach (var item in events)
+ {
+ var meetingCard = item.ToAdaptiveCardData();
+ var replyTemp = sc.Context.Activity.CreateAdaptiveCardReply(CalendarMainResponses.GreetingMessage, item.OnlineMeetingUrl == null ? "Dialogs/Shared/Resources/Cards/CalendarCardNoJoinButton.json" : "Dialogs/Shared/Resources/Cards/CalendarCard.json", meetingCard);
+ replyToConversation.Attachments.Add(replyTemp.Attachments[0]);
+ }
+
+ options.Prompt = replyToConversation;
+
+ return await sc.PromptAsync(Actions.EventChoice, options);
+ }
+ else
+ {
+ return await sc.EndDialogAsync(true);
+ }
+ }
+ catch
+ {
+ await sc.Context.SendActivityAsync(sc.Context.Activity.CreateReply(CalendarSharedResponses.CalendarErrorMessage, _responseBuilder));
+ var state = await _accessor.GetAsync(sc.Context);
+ state.Clear();
+ return await sc.CancelAllDialogsAsync();
+ }
+ }
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/ServiceClients/TimeZoneConverter.cs b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/ServiceClients/TimeZoneConverter.cs
new file mode 100644
index 0000000000..8757c9e04e
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/ServiceClients/TimeZoneConverter.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace CalendarSkill
+{
+ public static class TimeZoneConverter
+ {
+ private static IDictionary ianaToWindowsMap = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ private static IDictionary windowsToIanaMap = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ public static string IanaToWindows(string ianaTimeZoneId)
+ {
+ LoadData();
+ if (ianaToWindowsMap.ContainsKey(ianaTimeZoneId))
+ {
+ return ianaToWindowsMap[ianaTimeZoneId];
+ }
+
+ throw new InvalidTimeZoneException();
+ }
+
+ public static string WindowsToIana(string windowsTimeZoneId)
+ {
+ LoadData();
+ if (windowsToIanaMap.ContainsKey($"001|{windowsTimeZoneId}"))
+ {
+ return windowsToIanaMap[$"001|{windowsTimeZoneId}"];
+ }
+
+ throw new InvalidTimeZoneException();
+ }
+
+ private static void LoadData()
+ {
+ using (var mappingFile = new FileStream("ServiceClients/WindowsIanaMapping", FileMode.Open))
+ using (var sr = new StreamReader(mappingFile))
+ {
+ string line;
+ while ((line = sr.ReadLine()) != null)
+ {
+ var table = line.Split(",");
+ var windowsId = table[0];
+ var territory = table[1];
+ var ianaIdList = table[2].Split(" ");
+ if (!windowsToIanaMap.ContainsKey($"{territory}|{windowsId}"))
+ {
+ windowsToIanaMap.Add($"{territory}|{windowsId}", ianaIdList[0]);
+ }
+
+ foreach (var ianaId in ianaIdList)
+ {
+ if (!ianaToWindowsMap.ContainsKey(ianaId))
+ {
+ ianaToWindowsMap.Add(ianaId, windowsId);
+ }
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/Resources/WindowsIanaMapping b/solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/ServiceClients/WindowsIanaMapping
similarity index 100%
rename from solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/Resources/WindowsIanaMapping
rename to solutions/Virtual-Assistant/src/csharp/skills/CalendarSkill/ServiceClients/WindowsIanaMapping
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Connected Services/Application Insights/ConnectedService.json b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Connected Services/Application Insights/ConnectedService.json
new file mode 100644
index 0000000000..d15936a838
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Connected Services/Application Insights/ConnectedService.json
@@ -0,0 +1,7 @@
+{
+ "ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider",
+ "Version": "8.13.10627.1",
+ "GettingStartedDocument": {
+ "Uri": "https://go.microsoft.com/fwlink/?LinkID=798432"
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/DeploymentScripts/msbotClone/bot.recipe b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/DeploymentScripts/msbotClone/bot.recipe
new file mode 100644
index 0000000000..c3e266ae3b
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/DeploymentScripts/msbotClone/bot.recipe
@@ -0,0 +1,50 @@
+{
+ "version": "1.0",
+ "resources": [
+ {
+ "type": "endpoint",
+ "id": "1",
+ "name": "development",
+ "url": "http://localhost:3980/api/messages"
+ },
+ {
+ "type": "endpoint",
+ "id": "108",
+ "name": "production",
+ "url": "https://your-bot-url.azurewebsites.net/api/messages"
+ },
+ {
+ "type": "abs",
+ "id": "199",
+ "name": "ABS"
+ },
+ {
+ "type": "blob",
+ "id": "2",
+ "name": "AzStorage",
+ "container": "transcripts"
+ },
+ {
+ "type": "appInsights",
+ "id": "3",
+ "name": "AppInsights"
+ },
+ {
+ "type": "cosmosdb",
+ "id": "8",
+ "name": "CosmosDB",
+ "database": "botstate-db",
+ "collection": "botstate-collection"
+ },
+ {
+ "type": "luis",
+ "id": "general",
+ "name": "General"
+ },
+ {
+ "type": "luis",
+ "id": "email",
+ "name": "Email"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/emailskill/CognitiveModels/LUISModel.json b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/DeploymentScripts/msbotClone/email.luis
similarity index 100%
rename from solutions/Virtual-Assistant/src/csharp/skills/emailskill/CognitiveModels/LUISModel.json
rename to solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/DeploymentScripts/msbotClone/email.luis
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/DeploymentScripts/msbotClone/general.luis b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/DeploymentScripts/msbotClone/general.luis
new file mode 100644
index 0000000000..517a361102
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/DeploymentScripts/msbotClone/general.luis
@@ -0,0 +1,501 @@
+{
+ "name": "General",
+ "versionId": "0.1",
+ "desc": "",
+ "culture": "en-us",
+ "intents": [
+ {
+ "name": "Cancel"
+ },
+ {
+ "name": "ConfirmMore"
+ },
+ {
+ "name": "ConfirmNo"
+ },
+ {
+ "name": "ConfirmYes"
+ },
+ {
+ "name": "Escalate"
+ },
+ {
+ "name": "Goodbye"
+ },
+ {
+ "name": "Greeting"
+ },
+ {
+ "name": "Help"
+ },
+ {
+ "name": "Next"
+ },
+ {
+ "name": "None"
+ },
+ {
+ "name": "Restart"
+ }
+ ],
+ "entities": [],
+ "closedLists": [],
+ "composites": [],
+ "patternAnyEntities": [],
+ "regex_entities": [],
+ "prebuiltEntities": [
+ {
+ "name": "datetimeV2",
+ "roles": []
+ },
+ {
+ "name": "number",
+ "roles": []
+ }
+ ],
+ "regex_features": [],
+ "model_features": [],
+ "patterns": [],
+ "utterances": [
+ {
+ "text": "abort",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "absolutely",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "advance",
+ "intent": "Next",
+ "entities": []
+ },
+ {
+ "text": "by no means",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "bye",
+ "intent": "Goodbye",
+ "entities": []
+ },
+ {
+ "text": "can i do that again",
+ "intent": "Restart",
+ "entities": []
+ },
+ {
+ "text": "can i talk to a person",
+ "intent": "Escalate",
+ "entities": []
+ },
+ {
+ "text": "can you help me",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "cancel",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "contact support",
+ "intent": "Escalate",
+ "entities": []
+ },
+ {
+ "text": "definitely",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "disregard",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "don't",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "don't do it",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "don't do that",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "for sure",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "go back",
+ "intent": "Restart",
+ "entities": []
+ },
+ {
+ "text": "go forward",
+ "intent": "Next",
+ "entities": []
+ },
+ {
+ "text": "go to next",
+ "intent": "Next",
+ "entities": []
+ },
+ {
+ "text": "good evening",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "good morning",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "good night",
+ "intent": "Goodbye",
+ "entities": []
+ },
+ {
+ "text": "goodbye",
+ "intent": "Goodbye",
+ "entities": []
+ },
+ {
+ "text": "hello",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "hello bot",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "help",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "help me",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "help please",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "hi",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "hiya",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "how are you",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "how are you today",
+ "intent": "Greeting",
+ "entities": []
+ },
+ {
+ "text": "i don't understand",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "i need to change something",
+ "intent": "Restart",
+ "entities": []
+ },
+ {
+ "text": "i think so",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "i want to talk to a human",
+ "intent": "Escalate",
+ "entities": []
+ },
+ {
+ "text": "i would like to cancel",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "i would like to know more",
+ "intent": "ConfirmMore",
+ "entities": []
+ },
+ {
+ "text": "i'm stuck",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "let's do it",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "let's do that",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "luhan",
+ "intent": "None",
+ "entities": []
+ },
+ {
+ "text": "more",
+ "intent": "ConfirmMore",
+ "entities": []
+ },
+ {
+ "text": "more info",
+ "intent": "ConfirmMore",
+ "entities": []
+ },
+ {
+ "text": "more information",
+ "intent": "ConfirmMore",
+ "entities": []
+ },
+ {
+ "text": "my water bottle is green",
+ "intent": "None",
+ "entities": []
+ },
+ {
+ "text": "nah",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "never mind",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "next",
+ "intent": "Next",
+ "entities": []
+ },
+ {
+ "text": "next item",
+ "intent": "Next",
+ "entities": []
+ },
+ {
+ "text": "no",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "no thanks",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "no that's okay",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "no way",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "nope",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "not at all",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "not really",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "not right now thanks",
+ "intent": "ConfirmNo",
+ "entities": []
+ },
+ {
+ "text": "oh yes",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "ok",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "ok bye",
+ "intent": "Goodbye",
+ "entities": []
+ },
+ {
+ "text": "ok thanks",
+ "intent": "Goodbye",
+ "entities": []
+ },
+ {
+ "text": "okay",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "paper planes in the sky",
+ "intent": "None",
+ "entities": []
+ },
+ {
+ "text": "please stop",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "restart",
+ "intent": "Restart",
+ "entities": []
+ },
+ {
+ "text": "show me more",
+ "intent": "ConfirmMore",
+ "entities": []
+ },
+ {
+ "text": "show next",
+ "intent": "Next",
+ "entities": []
+ },
+ {
+ "text": "start over",
+ "intent": "Restart",
+ "entities": []
+ },
+ {
+ "text": "stop",
+ "intent": "Cancel",
+ "entities": []
+ },
+ {
+ "text": "sure",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "sure thing",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "talk to a human",
+ "intent": "Escalate",
+ "entities": []
+ },
+ {
+ "text": "that's it thanks",
+ "intent": "Goodbye",
+ "entities": []
+ },
+ {
+ "text": "transfer me please",
+ "intent": "Escalate",
+ "entities": []
+ },
+ {
+ "text": "turtles like to swim in the ocean",
+ "intent": "None",
+ "entities": []
+ },
+ {
+ "text": "undo",
+ "intent": "Restart",
+ "entities": []
+ },
+ {
+ "text": "what can i say",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "what can you do",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "what can you help me with",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "what do i do now",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "why doesn't this work",
+ "intent": "Help",
+ "entities": []
+ },
+ {
+ "text": "why not",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "yeah",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "yeah that would be great thanks",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "yep that's right",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "yes please",
+ "intent": "ConfirmYes",
+ "entities": []
+ },
+ {
+ "text": "yup",
+ "intent": "ConfirmYes",
+ "entities": []
+ }
+ ]
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Cancel/CancelDialog.cs b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Cancel/CancelDialog.cs
new file mode 100644
index 0000000000..26da446049
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Cancel/CancelDialog.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Bot.Builder.Dialogs;
+
+namespace EmailSkill
+{
+ public class CancelDialog : ComponentDialog
+ {
+ // Constants
+ public const string CancelPrompt = "cancelPrompt";
+
+ // Fields
+ private static CancelResponses _responder = new CancelResponses();
+
+ public CancelDialog()
+ : base(nameof(CancelDialog))
+ {
+ InitialDialogId = nameof(CancelDialog);
+
+ var cancel = new WaterfallStep[]
+ {
+ AskToCancel,
+ FinishCancelDialog,
+ };
+
+ AddDialog(new WaterfallDialog(InitialDialogId, cancel));
+ AddDialog(new ConfirmPrompt(CancelPrompt));
+ }
+
+ public static async Task AskToCancel(WaterfallStepContext sc, CancellationToken cancellationToken) => await sc.PromptAsync(CancelPrompt, new PromptOptions()
+ {
+ Prompt = await _responder.RenderTemplate(sc.Context, "en", CancelResponses._confirmPrompt),
+ });
+
+ public static async Task FinishCancelDialog(WaterfallStepContext sc, CancellationToken cancellationToken) => await sc.EndDialogAsync((bool)sc.Result);
+
+ protected override async Task EndComponentAsync(DialogContext outerDc, object result, CancellationToken cancellationToken)
+ {
+ var doCancel = (bool)result;
+
+ if (doCancel)
+ {
+ // If user chose to cancel
+ await _responder.ReplyWith(outerDc.Context, CancelResponses._cancelConfirmed);
+
+ // Cancel all in outer stack of component i.e. the stack the component belongs to
+ return await outerDc.CancelAllDialogsAsync();
+ }
+ else
+ {
+ // else if user chose not to cancel
+ await _responder.ReplyWith(outerDc.Context, CancelResponses._cancelDenied);
+
+ // End this component. Will trigger reprompt/resume on outer stack
+ return await outerDc.EndDialogAsync();
+ }
+ }
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Cancel/CancelResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Cancel/CancelResponses.cs
new file mode 100644
index 0000000000..9ec8f3431f
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Cancel/CancelResponses.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using EmailSkill.Dialogs.Cancel.Resources;
+using Microsoft.Bot.Builder.TemplateManager;
+
+namespace EmailSkill
+{
+ public class CancelResponses : TemplateManager
+ {
+ // Constants
+ public const string _confirmPrompt = "Cancel.ConfirmCancelPrompt";
+ public const string _cancelConfirmed = "Cancel.CancelConfirmed";
+ public const string _cancelDenied = "Cancel.CancelDenied";
+
+ // Fields
+ private static LanguageTemplateDictionary _responseTemplates = new LanguageTemplateDictionary
+ {
+ ["default"] = new TemplateIdMap
+ {
+ { _confirmPrompt, (context, data) => CancelStrings.CANCEL_PROMPT },
+ { _cancelConfirmed, (context, data) => CancelStrings.CANCEL_CONFIRMED },
+ { _cancelDenied, (context, data) => CancelStrings.CANCEL_DENIED },
+ },
+ ["en"] = new TemplateIdMap { },
+ ["fr"] = new TemplateIdMap { },
+ };
+
+ public CancelResponses()
+ {
+ Register(new DictionaryRenderer(_responseTemplates));
+ }
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Cancel/Resources/CancelStrings.Designer.cs b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Cancel/Resources/CancelStrings.Designer.cs
new file mode 100644
index 0000000000..2ffab16317
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Cancel/Resources/CancelStrings.Designer.cs
@@ -0,0 +1,105 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace EmailSkill.Dialogs.Cancel.Resources
+{
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class CancelStrings
+ {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal CancelStrings()
+ {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager
+ {
+ get
+ {
+ if (object.ReferenceEquals(resourceMan, null))
+ {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("EmailSkill.Dialogs.Cancel.Resources.CancelStrings", typeof(CancelStrings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Globalization.CultureInfo Culture
+ {
+ get
+ {
+ return resourceCulture;
+ }
+ set
+ {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Ok, let's start over..
+ ///
+ public static string CANCEL_CONFIRMED
+ {
+ get
+ {
+ return ResourceManager.GetString("CANCEL_CONFIRMED", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Ok, let's keep going..
+ ///
+ public static string CANCEL_DENIED
+ {
+ get
+ {
+ return ResourceManager.GetString("CANCEL_DENIED", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Are you sure you want to cancel?.
+ ///
+ public static string CANCEL_PROMPT
+ {
+ get
+ {
+ return ResourceManager.GetString("CANCEL_PROMPT", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/Resources/BotImages.resx b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Cancel/Resources/CancelStrings.resx
similarity index 59%
rename from solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/Resources/BotImages.resx
rename to solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Cancel/Resources/CancelStrings.resx
index 7ef0219cc2..8af874c85b 100644
--- a/solutions/Virtual-Assistant/src/csharp/skills/calendarskill/Dialogs/Shared/Resources/BotImages.resx
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Cancel/Resources/CancelStrings.resx
@@ -117,112 +117,13 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
- vendor/assistant-logo.jpg
+
+ Ok, let's start over.
-
- vendor/car-genius.png
+
+ Ok, let's keep going.
-
- vendor/concierge.jpg
-
-
- restaurants/cuisines/chinese.jpg
-
-
- restaurants/cuisines/german.jpg
-
-
- restaurants/cuisines/indian.jpg
-
-
- restaurants/cuisines/italian.jpg
-
-
- faq/blank.gif
-
-
- faq/BreakFluidRed.png
-
-
- faq/BreakFluidYellow.png
-
-
- faq/TirePressureRed.png
-
-
- faq/TirePressureYellow.png
-
-
- vendor/in-car-productivity.png
-
-
- vendor/microsoft/office365.png
-
-
- spa/plus.png
-
-
- productivity/calendar.png
-
-
- productivity/calendar-black.png
-
-
- productivity/todo.png
-
-
- productivity/todo-black.png
-
-
- vendor/push-to-talk.png
-
-
- restaurants/bamboo-garden.jpg
-
-
- restaurants/biryani-house.jpg
-
-
- restaurants/chens.jpg
-
-
- restaurants/euro-bistro.jpg
-
-
- restaurants/german-gourmet.jpg
-
-
- restaurants/kanishka-cuisine.jpg
-
-
- restaurants/maharani-inside.jpg
-
-
- restaurants/mamma-mia-pizza.jpg
-
-
- restaurants/mandarin.jpg
-
-
- restaurants/the-bavarian.jpg
-
-
- restaurants/tonys.jpg
-
-
- restaurants/tuscani-grill.jpg
-
-
- serviceScheduling/oil-change.jpg
-
-
- vendor/smart-interior.png
-
-
- spa/spa.jpg
-
-
- vendor/welcome-aboard.png
+
+ Are you sure you want to cancel?
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ConfirmRecipient/Resources/ConfirmRecipientResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ConfirmRecipient/Resources/ConfirmRecipientResponses.cs
new file mode 100644
index 0000000000..c5c87705c6
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ConfirmRecipient/Resources/ConfirmRecipientResponses.cs
@@ -0,0 +1,105 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace EmailSkill.Dialogs.ConfirmRecipient.Resources
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class ConfirmRecipientResponses
+ {
+ private const string JsonFileName = "ConfirmRecipientResponses.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+ public static BotResponse PromptTooManyPeople => GetBotResponse();
+
+ public static BotResponse PromptPersonNotFound => GetBotResponse();
+
+ public static BotResponse ConfirmRecipient => GetBotResponse();
+
+ public static BotResponse ConfirmRecipientNotFirstPage => GetBotResponse();
+
+ public static BotResponse ConfirmRecipientLastPage => GetBotResponse();
+
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(ConfirmRecipientResponses).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\ConfirmRecipient\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ConfirmRecipient/Resources/ConfirmRecipientResponses.en.json b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ConfirmRecipient/Resources/ConfirmRecipientResponses.en.json
new file mode 100644
index 0000000000..8b66a6de27
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ConfirmRecipient/Resources/ConfirmRecipientResponses.en.json
@@ -0,0 +1,71 @@
+{
+ "PromptTooManyPeople": {
+ "replies": [
+ {
+ "text": "There are too many people named {UserName}, Please re-enter the name.",
+ "speak": "There are too many people named {UserName}, Please re-enter the name."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "PromptPersonNotFound": {
+ "replies": [
+ {
+ "text": "Didn't find someone named {UserName}, please input the right name.",
+ "speak": "Didn't find someone named {UserName}, please input the right name."
+ },
+ {
+ "text": "I could not find the contact '{UserName}' your are looking for. Can you try again?",
+ "speak": "I could not find the contact '{UserName}' your are looking for. Can you try again?"
+ },
+ {
+ "text": "Can you try again by stating out firstname and surname?",
+ "speak": "Can you try again by stating out firstname and surname?"
+ },
+ {
+ "text": "Can you provide the alias of the contact you are looking for?",
+ "speak": "Can you provide the alias of the contact you are looking for?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "ConfirmRecipient": {
+ "replies": [
+ {
+ "text": "I found the following relevant contacts:",
+ "speak": "I found the following relevant contacts"
+ },
+ {
+ "text": "Which one do you want to email?",
+ "speak": "Which one do you want to email?"
+ },
+ {
+ "text": "Which contact do you want to select?",
+ "speak": "Which contact do you want to select?"
+ },
+ {
+ "text": "I found contacts that you work close with:",
+ "speak": "I found contacts that you work close with"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "ConfirmRecipientNotFirstPage": {
+ "replies": [
+ {
+ "text": "I found more relevant contacts, anyone you would like to select?",
+ "speak": "I found more relevant contacts, anyone you would like to select?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "ConfirmRecipientLastPage": {
+ "replies": [
+ {
+ "text": "I am showing you contacts within your organization. Anyone you would like to select?",
+ "speak": "I am showing you contacts within your organization. Anyone you would like to select?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ConfirmRecipient/Resources/ConfirmRecipientResponses.tt b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ConfirmRecipient/Resources/ConfirmRecipientResponses.tt
new file mode 100644
index 0000000000..6fdface391
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ConfirmRecipient/Resources/ConfirmRecipientResponses.tt
@@ -0,0 +1,108 @@
+<#@ assembly name="Newtonsoft.Json.dll" #>
+<#@ template debug="false" hostspecific="true" language="C#" #>
+<#@ output extension=".cs" #>
+<#
+ var className = System.IO.Path.GetFileNameWithoutExtension(Host.TemplateFile);
+ var namespaceName = System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint");;
+ string myFile = System.IO.File.ReadAllText(this.Host.ResolvePath(className + ".en.json"));
+ var json = Newtonsoft.Json.JsonConvert.DeserializeObject>(myFile);
+#>
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace <#= namespaceName #>
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class <#= className #>
+ {
+ private const string JsonFileName = "<#=className#>.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+<# foreach (var propertyName in json) { #>
+ public static BotResponse <#= propertyName.Key.Substring(0, 1).ToUpperInvariant() + propertyName.Key.Substring(1) #> => GetBotResponse();
+
+<# } #>
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(<#= className #>).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\ConfirmRecipient\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ForwardEmail/Resources/ForwardEmailResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ForwardEmail/Resources/ForwardEmailResponses.cs
new file mode 100644
index 0000000000..09bb4246cf
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ForwardEmail/Resources/ForwardEmailResponses.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace EmailSkill.Dialogs.ForwardEmail.Resources
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class ForwardEmailResponses
+ {
+ private const string JsonFileName = "ForwardEmailResponses.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(ForwardEmailResponses).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\ForwardEmail\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ForwardEmail/Resources/ForwardEmailResponses.en.json b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ForwardEmail/Resources/ForwardEmailResponses.en.json
new file mode 100644
index 0000000000..2c63c08510
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ForwardEmail/Resources/ForwardEmailResponses.en.json
@@ -0,0 +1,2 @@
+{
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ForwardEmail/Resources/ForwardEmailResponses.tt b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ForwardEmail/Resources/ForwardEmailResponses.tt
new file mode 100644
index 0000000000..034a75898f
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ForwardEmail/Resources/ForwardEmailResponses.tt
@@ -0,0 +1,108 @@
+<#@ assembly name="Newtonsoft.Json.dll" #>
+<#@ template debug="false" hostspecific="true" language="C#" #>
+<#@ output extension=".cs" #>
+<#
+ var className = System.IO.Path.GetFileNameWithoutExtension(Host.TemplateFile);
+ var namespaceName = System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint");;
+ string myFile = System.IO.File.ReadAllText(this.Host.ResolvePath(className + ".en.json"));
+ var json = Newtonsoft.Json.JsonConvert.DeserializeObject>(myFile);
+#>
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace <#= namespaceName #>
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class <#= className #>
+ {
+ private const string JsonFileName = "<#=className#>.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+<# foreach (var propertyName in json) { #>
+ public static BotResponse <#= propertyName.Key.Substring(0, 1).ToUpperInvariant() + propertyName.Key.Substring(1) #> => GetBotResponse();
+
+<# } #>
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(<#= className #>).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\ForwardEmail\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Main/MainDialog.cs b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Main/MainDialog.cs
new file mode 100644
index 0000000000..87e022ad13
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Main/MainDialog.cs
@@ -0,0 +1,262 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using EmailSkill.Dialogs.Main.Resources;
+using EmailSkill.Dialogs.Shared.Resources;
+using Luis;
+using Microsoft.Bot.Builder;
+using Microsoft.Bot.Builder.Dialogs;
+using Microsoft.Bot.Schema;
+using Microsoft.Bot.Solutions;
+using Microsoft.Bot.Solutions.Dialogs;
+using Microsoft.Bot.Solutions.Extensions;
+using Microsoft.Bot.Solutions.Skills;
+
+namespace EmailSkill
+{
+ public class MainDialog : RouterDialog
+ {
+ private bool _skillMode;
+ private SkillConfiguration _services;
+ private UserState _userState;
+ private ConversationState _conversationState;
+ private IMailSkillServiceManager _serviceManager;
+ private IStatePropertyAccessor _stateAccessor;
+ private IStatePropertyAccessor _dialogStateAccessor;
+ private EmailSkillResponseBuilder _responseBuilder = new EmailSkillResponseBuilder();
+
+ public MainDialog(SkillConfiguration services, ConversationState conversationState, UserState userState, IMailSkillServiceManager serviceManager, bool skillMode)
+ : base(nameof(MainDialog))
+ {
+ _skillMode = skillMode;
+ _services = services;
+ _conversationState = conversationState;
+ _userState = userState;
+ _serviceManager = serviceManager;
+
+ // Initialize state accessor
+ _stateAccessor = _conversationState.CreateProperty(nameof(EmailSkillState));
+ _dialogStateAccessor = _conversationState.CreateProperty(nameof(DialogState));
+
+ RegisterDialogs();
+ }
+
+ protected override async Task OnStartAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (!_skillMode)
+ {
+ // send a greeting if we're in local mode
+ await dc.Context.SendActivityAsync(dc.Context.Activity.CreateReply(EmailMainResponses.EmailWelcomeMessage));
+ }
+ }
+
+ protected override async Task RouteAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var state = await _stateAccessor.GetAsync(dc.Context, () => new EmailSkillState());
+
+ // If dispatch result is general luis model
+ _services.LuisServices.TryGetValue("email", out var luisService);
+
+ if (luisService == null)
+ {
+ throw new Exception("The specified LUIS Model could not be found in your Bot Services configuration.");
+ }
+ else
+ {
+ var result = await luisService.RecognizeAsync(dc.Context, CancellationToken.None);
+ var intent = result?.TopIntent().intent;
+
+ var skillOptions = new EmailSkillDialogOptions
+ {
+ SkillMode = _skillMode,
+ };
+
+ // switch on general intents
+ switch (intent)
+ {
+ case Email.Intent.SendEmail:
+ {
+ await dc.BeginDialogAsync(nameof(SendEmailDialog), skillOptions);
+ break;
+ }
+
+ case Email.Intent.Forward:
+ {
+ await dc.BeginDialogAsync(nameof(ForwardEmailDialog), skillOptions);
+ break;
+ }
+
+ case Email.Intent.Reply:
+ {
+ await dc.BeginDialogAsync(nameof(ReplyEmailDialog), skillOptions);
+ break;
+ }
+
+ case Email.Intent.SearchMessages:
+ case Email.Intent.ShowNext:
+ case Email.Intent.ShowPrevious:
+ case Email.Intent.CheckMessages:
+ {
+ await dc.BeginDialogAsync(nameof(ShowEmailDialog), skillOptions);
+ break;
+ }
+
+ case Email.Intent.None:
+ {
+ await dc.Context.SendActivityAsync(dc.Context.Activity.CreateReply(EmailSharedResponses.DidntUnderstandMessage));
+ if (_skillMode)
+ {
+ await CompleteAsync(dc);
+ }
+
+ break;
+ }
+
+ default:
+ {
+ await dc.Context.SendActivityAsync(dc.Context.Activity.CreateReply(EmailMainResponses.FeatureNotAvailable));
+
+ if (_skillMode)
+ {
+ await CompleteAsync(dc);
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ protected override async Task CompleteAsync(DialogContext dc, DialogTurnResult result = null, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (_skillMode)
+ {
+ var response = dc.Context.Activity.CreateReply();
+ response.Type = ActivityTypes.EndOfConversation;
+
+ await dc.Context.SendActivityAsync(response);
+ }
+ else
+ {
+ await dc.Context.SendActivityAsync(dc.Context.Activity.CreateReply(EmailSharedResponses.ActionEnded));
+ }
+
+ // End active dialog
+ await dc.EndDialogAsync(result);
+ }
+
+ protected override async Task OnEventAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (dc.Context.Activity.Name == "tokens/response")
+ {
+ // Auth dialog completion
+ var result = await dc.ContinueDialogAsync();
+
+ // If the dialog completed when we sent the token, end the skill conversation
+ if (result.Status != DialogTurnStatus.Waiting)
+ {
+ var response = dc.Context.Activity.CreateReply();
+ response.Type = ActivityTypes.EndOfConversation;
+
+ await dc.Context.SendActivityAsync(response);
+ }
+ }
+ }
+
+ protected override async Task OnInterruptDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var result = InterruptionAction.NoAction;
+
+ if (dc.Context.Activity.Type == ActivityTypes.Message)
+ {
+ // Update state with email luis result and entities
+ var emailLuisResult = await _services.LuisServices["email"].RecognizeAsync(dc.Context, cancellationToken);
+ var state = await _stateAccessor.GetAsync(dc.Context, () => new EmailSkillState());
+ state.LuisResult = emailLuisResult;
+
+ // check luis intent
+ _services.LuisServices.TryGetValue("general", out var luisService);
+
+ if (luisService == null)
+ {
+ throw new Exception("The specified LUIS Model could not be found in your Skill configuration.");
+ }
+ else
+ {
+ var luisResult = await luisService.RecognizeAsync(dc.Context, cancellationToken);
+ var topIntent = luisResult.TopIntent().intent;
+
+ // check intent
+ switch (topIntent)
+ {
+ case General.Intent.Cancel:
+ {
+ result = await OnCancel(dc);
+ break;
+ }
+
+ case General.Intent.Help:
+ {
+ result = await OnHelp(dc);
+ break;
+ }
+
+ case General.Intent.Logout:
+ {
+ result = await OnLogout(dc);
+ break;
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private async Task OnCancel(DialogContext dc)
+ {
+ await dc.BeginDialogAsync(nameof(CancelDialog));
+ return InterruptionAction.StartedDialog;
+ }
+
+ private async Task OnHelp(DialogContext dc)
+ {
+ await dc.Context.SendActivityAsync(dc.Context.Activity.CreateReply(EmailMainResponses.HelpMessage));
+ return InterruptionAction.MessageSentToUser;
+ }
+
+ private async Task OnLogout(DialogContext dc)
+ {
+ BotFrameworkAdapter adapter;
+ var supported = dc.Context.Adapter is BotFrameworkAdapter;
+ if (!supported)
+ {
+ throw new InvalidOperationException("OAuthPrompt.SignOutUser(): not supported by the current adapter");
+ }
+ else
+ {
+ adapter = (BotFrameworkAdapter)dc.Context.Adapter;
+ }
+
+ await dc.CancelAllDialogsAsync();
+
+ // Sign out user
+ await adapter.SignOutUserAsync(dc.Context, _services.AuthConnectionName);
+ await dc.Context.SendActivityAsync(dc.Context.Activity.CreateReply(EmailMainResponses.LogOut));
+
+ return InterruptionAction.StartedDialog;
+ }
+
+ private void RegisterDialogs()
+ {
+ AddDialog(new ForwardEmailDialog(_services, _stateAccessor, _dialogStateAccessor, _serviceManager));
+ AddDialog(new SendEmailDialog(_services, _stateAccessor, _dialogStateAccessor, _serviceManager));
+ AddDialog(new ShowEmailDialog(_services, _stateAccessor, _dialogStateAccessor, _serviceManager));
+ AddDialog(new ReplyEmailDialog(_services, _stateAccessor, _dialogStateAccessor, _serviceManager));
+ AddDialog(new CancelDialog());
+ }
+ }
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Main/Resources/EmailMainResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Main/Resources/EmailMainResponses.cs
new file mode 100644
index 0000000000..6db4ccd65b
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Main/Resources/EmailMainResponses.cs
@@ -0,0 +1,105 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace EmailSkill.Dialogs.Main.Resources
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class EmailMainResponses
+ {
+ private const string JsonFileName = "EmailMainResponses.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+ public static BotResponse EmailWelcomeMessage => GetBotResponse();
+
+ public static BotResponse HelpMessage => GetBotResponse();
+
+ public static BotResponse GreetingMessage => GetBotResponse();
+
+ public static BotResponse LogOut => GetBotResponse();
+
+ public static BotResponse FeatureNotAvailable => GetBotResponse();
+
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(EmailMainResponses).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\Main\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Main/Resources/EmailMainResponses.en.json b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Main/Resources/EmailMainResponses.en.json
new file mode 100644
index 0000000000..a9d3b3dea3
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Main/Resources/EmailMainResponses.en.json
@@ -0,0 +1,72 @@
+{
+ "EmailWelcomeMessage": {
+ "replies": [
+ {
+ "text": "Welcome! I am the Email Skill. I can help you manage your Office 365 email, show your unread email, reply or forward email for you.",
+ "speak": "Welcome! I am the Email Skill. I can help you manage your Office 365 email, show your unread email, reply or forward email for you."
+ }
+ ],
+ "suggestedActions": [
+ "Show latest email",
+ "Send an email to somebody"
+ ],
+ "inputHint": "expectingInput"
+ },
+ "HelpMessage": {
+ "replies": [
+ {
+ "text": "I can provide you an overview of your latest emails, read out important emails to you, or create and forward a new email for you.",
+ "speak": " I can provide you an overview of your latest emails, read out important emails to you, or create and forward a new email for you."
+ }
+ ],
+ "suggestedActions": [
+ "Show Latest Email",
+ "Send an email to somebody",
+ "Forward the second email to somebody"
+ ],
+ "inputHint": "expectingInput"
+ },
+ "GreetingMessage": {
+ "replies": [
+ {
+ "text": "Hi!",
+ "speak": "Hi!"
+ },
+ {
+ "text": "I'm here!",
+ "speak": "I'm here!"
+ },
+ {
+ "text": "Hi there!",
+ "speak": "Hi there!"
+ }
+ ],
+ "inputHint": "acceptingInput"
+ },
+ "LogOut": {
+ "replies": [
+ {
+ "text": "Your sign out was successful.",
+ "speak": "Your sign out was successful."
+ },
+ {
+ "text": "You have successfully sign out.",
+ "speak": "You have successfully sign out."
+ },
+ {
+ "text": "You have been logged out.",
+ "speak": "You have been logged out."
+ }
+ ],
+ "inputHint": "acceptingInput"
+ },
+ "FeatureNotAvailable": {
+ "replies": [
+ {
+ "text": "This feature is not yet available in the Email Skill. Please try asking something else.",
+ "speak": "This feature is not yet available in the Email Skill. Please try asking something else."
+ }
+ ],
+ "inputHint": "acceptingInput"
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Main/Resources/EmailMainResponses.tt b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Main/Resources/EmailMainResponses.tt
new file mode 100644
index 0000000000..3e815afb84
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/Main/Resources/EmailMainResponses.tt
@@ -0,0 +1,108 @@
+<#@ assembly name="Newtonsoft.Json.dll" #>
+<#@ template debug="false" hostspecific="true" language="C#" #>
+<#@ output extension=".cs" #>
+<#
+ var className = System.IO.Path.GetFileNameWithoutExtension(Host.TemplateFile);
+ var namespaceName = System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint");;
+ string myFile = System.IO.File.ReadAllText(this.Host.ResolvePath(className + ".en.json"));
+ var json = Newtonsoft.Json.JsonConvert.DeserializeObject>(myFile);
+#>
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace <#= namespaceName #>
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class <#= className #>
+ {
+ private const string JsonFileName = "<#=className#>.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+<# foreach (var propertyName in json) { #>
+ public static BotResponse <#= propertyName.Key.Substring(0, 1).ToUpperInvariant() + propertyName.Key.Substring(1) #> => GetBotResponse();
+
+<# } #>
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(<#= className #>).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\Main\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ReplyEmail/Resources/ReplyEmailResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ReplyEmail/Resources/ReplyEmailResponses.cs
new file mode 100644
index 0000000000..3fb469a103
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ReplyEmail/Resources/ReplyEmailResponses.cs
@@ -0,0 +1,129 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace EmailSkill.Dialogs.ReplyEmail.Resources
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class ReplyEmailResponses
+ {
+ private const string JsonFileName = "ReplyEmailResponses.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+ public static BotResponse NoTitle => GetBotResponse();
+
+ public static BotResponse NoContent => GetBotResponse();
+
+ public static BotResponse NoLocation => GetBotResponse();
+
+ public static BotResponse ConfirmCreate => GetBotResponse();
+
+ public static BotResponse ConfirmCreateFailed => GetBotResponse();
+
+ public static BotResponse EventCreated => GetBotResponse();
+
+ public static BotResponse EventCreationFailed => GetBotResponse();
+
+ public static BotResponse NoAttendeesMS => GetBotResponse();
+
+ public static BotResponse WrongAddress => GetBotResponse();
+
+ public static BotResponse NoAttendees => GetBotResponse();
+
+ public static BotResponse PromptTooManyPeople => GetBotResponse();
+
+ public static BotResponse PromptPersonNotFound => GetBotResponse();
+
+ public static BotResponse NoStartDate => GetBotResponse();
+
+ public static BotResponse NoStartTime => GetBotResponse();
+
+ public static BotResponse NoDuration => GetBotResponse();
+
+ public static BotResponse FindUserErrorMessage => GetBotResponse();
+
+ public static BotResponse ConfirmRecipient => GetBotResponse();
+
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(ReplyEmailResponses).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\ReplyEmail\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ReplyEmail/Resources/ReplyEmailResponses.en.json b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ReplyEmail/Resources/ReplyEmailResponses.en.json
new file mode 100644
index 0000000000..2c63c08510
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ReplyEmail/Resources/ReplyEmailResponses.en.json
@@ -0,0 +1,2 @@
+{
+}
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ReplyEmail/Resources/ReplyEmailResponses.tt b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ReplyEmail/Resources/ReplyEmailResponses.tt
new file mode 100644
index 0000000000..da6a7ede35
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/ReplyEmail/Resources/ReplyEmailResponses.tt
@@ -0,0 +1,108 @@
+<#@ assembly name="Newtonsoft.Json.dll" #>
+<#@ template debug="false" hostspecific="true" language="C#" #>
+<#@ output extension=".cs" #>
+<#
+ var className = System.IO.Path.GetFileNameWithoutExtension(Host.TemplateFile);
+ var namespaceName = System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint");;
+ string myFile = System.IO.File.ReadAllText(this.Host.ResolvePath(className + ".en.json"));
+ var json = Newtonsoft.Json.JsonConvert.DeserializeObject>(myFile);
+#>
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace <#= namespaceName #>
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class <#= className #>
+ {
+ private const string JsonFileName = "<#=className#>.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+<# foreach (var propertyName in json) { #>
+ public static BotResponse <#= propertyName.Key.Substring(0, 1).ToUpperInvariant() + propertyName.Key.Substring(1) #> => GetBotResponse();
+
+<# } #>
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(<#= className #>).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\ReplyEmail\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/SendEmail/Resources/SendEmailResponses.cs b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/SendEmail/Resources/SendEmailResponses.cs
new file mode 100644
index 0000000000..e4f79605cc
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/SendEmail/Resources/SendEmailResponses.cs
@@ -0,0 +1,101 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace EmailSkill.Dialogs.SendEmail.Resources
+{
+ ///
+ /// Calendar bot responses class.
+ ///
+ public static class SendEmailResponses
+ {
+ private const string JsonFileName = "SendEmailResponses.*.json";
+
+ private static Dictionary> jsonResponses;
+
+ // Generated code:
+ // This code runs in the text json:
+ public static BotResponse RecipientConfirmed => GetBotResponse();
+
+ public static BotResponse NoSubject => GetBotResponse();
+
+ public static BotResponse NoMessageBody => GetBotResponse();
+
+ private static Dictionary> JsonResponses
+ {
+ get
+ {
+ if (jsonResponses != null)
+ {
+ return jsonResponses;
+ }
+
+ jsonResponses = new Dictionary>();
+ var dir = Path.GetDirectoryName(typeof(SendEmailResponses).Assembly.Location);
+ var resDir = Path.Combine(dir, "Dialogs\\SendEmail\\Resources");
+
+ var jsonFiles = Directory.GetFiles(resDir, JsonFileName);
+ foreach (var file in jsonFiles)
+ {
+ var jsonData = File.ReadAllText(file);
+ var responses = JsonConvert.DeserializeObject>(jsonData);
+ var key = new FileInfo(file).Name.Split(".")[1].ToLower();
+
+ jsonResponses.Add(key, responses);
+ }
+
+ return jsonResponses;
+ }
+ }
+
+ private static BotResponse GetBotResponse([CallerMemberName] string propertyName = null)
+ {
+ var locale = CultureInfo.CurrentUICulture.Name;
+ var theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to parent language
+ if (theK == null)
+ {
+ locale = CultureInfo.CurrentUICulture.Name.Split("-")[0].ToLower();
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+
+ // fall back to en
+ if (theK == null)
+ {
+ locale = "en";
+ theK = GetJsonResponseKeyForLocale(locale, propertyName);
+ }
+ }
+
+ var botResponse = JsonResponses[locale][theK ?? throw new ArgumentNullException(nameof(propertyName))];
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(botResponse));
+ }
+
+ private static string GetJsonResponseKeyForLocale(string locale, string propertyName)
+ {
+ try
+ {
+ if (JsonResponses.ContainsKey(locale))
+ {
+ return JsonResponses[locale].ContainsKey(propertyName) ?
+ JsonResponses[locale].Keys.FirstOrDefault(k => string.Compare(k, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) :
+ null;
+ }
+
+ return null;
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/SendEmail/Resources/SendEmailResponses.en.json b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/SendEmail/Resources/SendEmailResponses.en.json
new file mode 100644
index 0000000000..8f55eb6925
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/SendEmail/Resources/SendEmailResponses.en.json
@@ -0,0 +1,31 @@
+{
+ "RecipientConfirmed": {
+ "replies": [
+ {
+ "text": "Alright, I will send this to {UserName}.",
+ "speak": "Alright, I will send this to {UserName}."
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoSubject": {
+ "replies": [
+ {
+ "text": "What is the email subject?",
+ "speak": "What is the email subject?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ },
+ "NoMessageBody": {
+ "replies": [
+
+ {
+ "text": "What's the message?",
+ "speak": "What's the message?"
+ }
+ ],
+ "inputHint": "expectingInput"
+ }
+
+}
\ No newline at end of file
diff --git a/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/SendEmail/Resources/SendEmailResponses.tt b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/SendEmail/Resources/SendEmailResponses.tt
new file mode 100644
index 0000000000..e696b61972
--- /dev/null
+++ b/solutions/Virtual-Assistant/src/csharp/skills/EmailSkill/Dialogs/SendEmail/Resources/SendEmailResponses.tt
@@ -0,0 +1,108 @@
+<#@ assembly name="Newtonsoft.Json.dll" #>
+<#@ template debug="false" hostspecific="true" language="C#" #>
+<#@ output extension=".cs" #>
+<#
+ var className = System.IO.Path.GetFileNameWithoutExtension(Host.TemplateFile);
+ var namespaceName = System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint");;
+ string myFile = System.IO.File.ReadAllText(this.Host.ResolvePath(className + ".en.json"));
+ var json = Newtonsoft.Json.JsonConvert.DeserializeObject>(myFile);
+#>
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.Bot.Solutions.Dialogs;
+using Newtonsoft.Json;
+
+namespace <#= namespaceName #>
+{
+ ///