From 71a6e9a43b0763a2b2f814164be6a4f783ed944c Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Tue, 6 Aug 2019 14:04:04 +0800 Subject: [PATCH 01/15] add itsm skill with create ticket action --- .../experimental/itsmskill/.filenesting.json | 15 + .../itsmskill/Adapters/CustomSkillAdapter.cs | 43 ++ .../itsmskill/Adapters/DefaultAdapter.cs | 43 ++ .../experimental/itsmskill/Bots/DialogBot.cs | 46 ++ .../itsmskill/Content/Knowledge.1.0.json | 52 ++ .../itsmskill/Content/Knowledge.json | 68 +++ .../itsmskill/Content/Ticket.1.0.json | 81 +++ .../itsmskill/Content/Ticket.json | 96 ++++ .../itsmskill/Controllers/BotController.cs | 24 + .../Deployment/Resources/LU/en/ITSM.lu | 20 + .../Resources/LU/en/Ticket/Create.lu | 20 + .../Deployment/Resources/LU/en/general.lu | 521 ++++++++++++++++++ .../Resources/parameters.template.json | 24 + .../Deployment/Resources/template.json | 245 ++++++++ .../itsmskill/Deployment/Scripts/deploy.ps1 | 245 ++++++++ .../Scripts/deploy_cognitive_models.ps1 | 300 ++++++++++ .../Deployment/Scripts/luis_functions.ps1 | 112 ++++ .../itsmskill/Deployment/Scripts/publish.ps1 | 66 +++ .../Deployment/Scripts/qna_functions.ps1 | 87 +++ .../Scripts/update_cognitive_models.ps1 | 159 ++++++ .../itsmskill/Dialogs/CreateTicketDialog.cs | 265 +++++++++ .../itsmskill/Dialogs/MainDialog.cs | 267 +++++++++ .../itsmskill/Dialogs/SkillDialogBase.cs | 239 ++++++++ .../experimental/itsmskill/ITSMSkill.csproj | 124 +++++ .../itsmskill/Models/CreateTicketResult.cs | 12 + .../itsmskill/Models/Knowledge.cs | 18 + .../itsmskill/Models/KnowledgeCard.cs | 17 + .../itsmskill/Models/ResultBase.cs | 14 + .../itsmskill/Models/SearchKnowledgeResult.cs | 12 + .../itsmskill/Models/SearchTicketResult.cs | 12 + .../Models/ServiceNow/CreateTicketRequest.cs | 9 + .../Models/ServiceNow/CreateTicketResponse.cs | 12 + .../Models/ServiceNow/KnowledgeResponse.cs | 20 + .../ServiceNow/SearchKnowledgeResponse.cs | 12 + .../Models/ServiceNow/SearchTicketResponse.cs | 12 + .../Models/ServiceNow/TicketResponse.cs | 31 ++ .../itsmskill/Models/SkillState.cs | 39 ++ .../experimental/itsmskill/Models/Ticket.cs | 22 + .../itsmskill/Models/TicketCard.cs | 23 + .../itsmskill/Models/TicketState.cs | 19 + .../itsmskill/Models/UrgencyLevel.cs | 16 + .../csharp/experimental/itsmskill/Program.cs | 21 + .../itsmskill/Properties/launchSettings.json | 27 + .../CreateTicket/CreateTicketResponses.cs | 17 + .../CreateTicket/CreateTicketResponses.json | 11 + .../CreateTicket/CreateTicketResponses.tt | 3 + .../itsmskill/Responses/Main/MainResponses.cs | 23 + .../Responses/Main/MainResponses.json | 83 +++ .../itsmskill/Responses/Main/MainResponses.tt | 3 + .../Responses/Shared/ResponseIdCollection.t4 | 31 ++ .../Responses/Shared/SharedResponses.cs | 29 + .../Responses/Shared/SharedResponses.json | 211 +++++++ .../Responses/Shared/SharedResponses.tt | 3 + .../Shared/SharedStrings.Designer.cs | 180 ++++++ .../Responses/Shared/SharedStrings.resx | 159 ++++++ .../itsmskill/Services/BotServices.cs | 69 +++ .../itsmskill/Services/BotSettings.cs | 14 + .../itsmskill/Services/GeneralLuis.cs | 88 +++ .../Services/IITServiceManagement.cs | 16 + .../itsmskill/Services/IServiceManager.cs | 9 + .../itsmskill/Services/ITSMLuis.cs | 66 +++ .../itsmskill/Services/ServiceManager.cs | 19 + .../Services/ServiceNow/Management.cs | 232 ++++++++ .../csharp/experimental/itsmskill/Startup.cs | 133 +++++ .../experimental/itsmskill/appsettings.json | 25 + .../itsmskill/cognitivemodels.json | 29 + .../itsmskill/manifestTemplate.json | 37 ++ .../csharp/experimental/itsmskill/readme.md | 12 + .../itsmskill/wwwroot/default.htm | 426 ++++++++++++++ 69 files changed, 5438 insertions(+) create mode 100644 skills/src/csharp/experimental/itsmskill/.filenesting.json create mode 100644 skills/src/csharp/experimental/itsmskill/Adapters/CustomSkillAdapter.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Adapters/DefaultAdapter.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Bots/DialogBot.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Content/Knowledge.1.0.json create mode 100644 skills/src/csharp/experimental/itsmskill/Content/Knowledge.json create mode 100644 skills/src/csharp/experimental/itsmskill/Content/Ticket.1.0.json create mode 100644 skills/src/csharp/experimental/itsmskill/Content/Ticket.json create mode 100644 skills/src/csharp/experimental/itsmskill/Controllers/BotController.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Create.lu create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/general.lu create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Resources/parameters.template.json create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Resources/template.json create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Scripts/deploy.ps1 create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Scripts/deploy_cognitive_models.ps1 create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Scripts/luis_functions.ps1 create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Scripts/publish.ps1 create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Scripts/qna_functions.ps1 create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Scripts/update_cognitive_models.ps1 create mode 100644 skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs create mode 100644 skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj create mode 100644 skills/src/csharp/experimental/itsmskill/Models/CreateTicketResult.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/Knowledge.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/KnowledgeCard.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/ResultBase.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/SearchKnowledgeResult.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/SearchTicketResult.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketResponse.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/ServiceNow/KnowledgeResponse.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchKnowledgeResponse.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchTicketResponse.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/ServiceNow/TicketResponse.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/SkillState.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/Ticket.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/TicketCard.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/TicketState.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/UrgencyLevel.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Program.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Properties/launchSettings.json create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.json create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.tt create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.json create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.tt create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/Shared/ResponseIdCollection.t4 create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.tt create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx create mode 100644 skills/src/csharp/experimental/itsmskill/Services/BotServices.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Services/BotSettings.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Services/GeneralLuis.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Services/IServiceManager.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Services/ServiceManager.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Startup.cs create mode 100644 skills/src/csharp/experimental/itsmskill/appsettings.json create mode 100644 skills/src/csharp/experimental/itsmskill/cognitivemodels.json create mode 100644 skills/src/csharp/experimental/itsmskill/manifestTemplate.json create mode 100644 skills/src/csharp/experimental/itsmskill/readme.md create mode 100644 skills/src/csharp/experimental/itsmskill/wwwroot/default.htm diff --git a/skills/src/csharp/experimental/itsmskill/.filenesting.json b/skills/src/csharp/experimental/itsmskill/.filenesting.json new file mode 100644 index 0000000000..90c31b9e8a --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/.filenesting.json @@ -0,0 +1,15 @@ +{ + "help": "https://go.microsoft.com/fwlink/?linkid=866610", + "dependentFileProviders": { + "add": { + "pathSegment": { + "add": { + ".*": [ + ".json", + ".resx" + ] + } + } + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Adapters/CustomSkillAdapter.cs b/skills/src/csharp/experimental/itsmskill/Adapters/CustomSkillAdapter.cs new file mode 100644 index 0000000000..41470fdfcb --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Adapters/CustomSkillAdapter.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Globalization; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Azure; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Builder.Solutions.Middleware; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Schema; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Services; + +namespace ITSMSkill.Adapters +{ + public class CustomSkillAdapter : SkillWebSocketBotAdapter + { + public CustomSkillAdapter( + BotSettings settings, + UserState userState, + ConversationState conversationState, + ResponseManager responseManager, + IBotTelemetryClient telemetryClient) + { + OnTurnError = async (context, exception) => + { + CultureInfo.CurrentUICulture = new CultureInfo(context.Activity.Locale); + await context.SendActivityAsync(responseManager.GetResponse(SharedResponses.ErrorMessage)); + await context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"Skill Error: {exception.Message} | {exception.StackTrace}")); + telemetryClient.TrackException(exception); + }; + + // Uncomment the following line for local development without Azure Storage + // Use(new TranscriptLoggerMiddleware(new MemoryTranscriptStore())); + Use(new TranscriptLoggerMiddleware(new AzureBlobTranscriptStore(settings.BlobStorage.ConnectionString, settings.BlobStorage.Container))); + Use(new TelemetryLoggerMiddleware(telemetryClient, logPersonalInformation: true)); + Use(new SetLocaleMiddleware(settings.DefaultLocale ?? "en-us")); + Use(new EventDebuggerMiddleware()); + Use(new SkillMiddleware(userState, conversationState, conversationState.CreateProperty(nameof(ITSMSkill)))); + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Adapters/DefaultAdapter.cs b/skills/src/csharp/experimental/itsmskill/Adapters/DefaultAdapter.cs new file mode 100644 index 0000000000..55dad2d848 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Adapters/DefaultAdapter.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Globalization; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Azure; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Builder.Solutions.Middleware; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Bot.Schema; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Services; + +namespace ITSMSkill.Bots +{ + public class DefaultAdapter : BotFrameworkHttpAdapter + { + public DefaultAdapter( + BotSettings settings, + ICredentialProvider credentialProvider, + IBotTelemetryClient telemetryClient, + ResponseManager responseManager) + : base(credentialProvider) + { + OnTurnError = async (context, exception) => + { + CultureInfo.CurrentUICulture = new CultureInfo(context.Activity.Locale); + await context.SendActivityAsync(responseManager.GetResponse(SharedResponses.ErrorMessage)); + await context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"Skill Error: {exception.Message} | {exception.StackTrace}")); + telemetryClient.TrackException(exception); + }; + + // Uncomment the following line for local development without Azure Storage + // Use(new TranscriptLoggerMiddleware(new MemoryTranscriptStore())); + Use(new TranscriptLoggerMiddleware(new AzureBlobTranscriptStore(settings.BlobStorage.ConnectionString, settings.BlobStorage.Container))); + Use(new TelemetryLoggerMiddleware(telemetryClient, logPersonalInformation: true)); + Use(new ShowTypingMiddleware()); + Use(new SetLocaleMiddleware(settings.DefaultLocale ?? "en-us")); + Use(new EventDebuggerMiddleware()); + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Bots/DialogBot.cs b/skills/src/csharp/experimental/itsmskill/Bots/DialogBot.cs new file mode 100644 index 0000000000..5c4a3e1b12 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Bots/DialogBot.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.DependencyInjection; + +namespace ITSMSkill.Bots +{ + public class DialogBot : IBot + where T : Dialog + { + private readonly Dialog _dialog; + private readonly BotState _conversationState; + private readonly BotState _userState; + private readonly IBotTelemetryClient _telemetryClient; + + public DialogBot(IServiceProvider serviceProvider, T dialog) + { + _dialog = dialog; + _conversationState = serviceProvider.GetService(); + _userState = serviceProvider.GetService(); + _telemetryClient = serviceProvider.GetService(); + } + + public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) + { + // Client notifying this bot took to long to respond (timed out) + if (turnContext.Activity.Code == EndOfConversationCodes.BotTimedOut) + { + _telemetryClient.TrackTrace($"Timeout in {turnContext.Activity.ChannelId} channel: Bot took too long to respond.", Severity.Information, null); + return; + } + + await _dialog.RunAsync(turnContext, _conversationState.CreateProperty(nameof(DialogState)), cancellationToken); + + // Save any state changes that might have occured during the turn. + await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken); + await _userState.SaveChangesAsync(turnContext, false, cancellationToken); + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Content/Knowledge.1.0.json b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.1.0.json new file mode 100644 index 0000000000..4f2d1a459e --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.1.0.json @@ -0,0 +1,52 @@ +{ + "type": "AdaptiveCard", + "id": "TicketCard", + "body": [ + { + "type": "Container", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "verticalContentAlignment": "Center", + "items": [ + { + "type": "TextBlock", + "horizontalAlignment": "Left", + "size": "Medium", + "color": "Default", + "text": "{Title}", + "wrap": true + } + ], + "width": "stretch" + } + ] + }, + { + "type": "TextBlock", + "size": "Small", + "color": "Default", + "text": "{UpdatedTime}" + }, + { + "type": "TextBlock", + "size": "Small", + "color": "Default", + "text": "ID: {Id}" + } + ] + }, + { + "type": "TextBlock", + "wrap": true, + "text": "{Content}", + "maxLines": 5 + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.0", + "speak": "{Speak}" +} diff --git a/skills/src/csharp/experimental/itsmskill/Content/Knowledge.json b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.json new file mode 100644 index 0000000000..4a23df3ca8 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.json @@ -0,0 +1,68 @@ +{ + "type": "AdaptiveCard", + "id": "TicketCard", + "body": [ + { + "type": "Container", + "backgroundImage": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMzA5LjA1IDMwOS4wNSI+PGRlZnM+PHN0eWxlPi5he2ZpbGw6IzY1YWZlMjt9LmJ7ZmlsbDojMDA1OGE4O30uYiwuZHtvcGFjaXR5OjAuMjU7fS5jLC5le29wYWNpdHk6MC4yO30uY3tmaWxsOnVybCgjYSk7fS5ke2ZpbGw6I2I2ZGNmMTt9LmV7ZmlsbDp1cmwoI2IpO308L3N0eWxlPjxsaW5lYXJHcmFkaWVudCBpZD0iYSIgeDE9IjI0MS4wMyIgeTE9IjEwNS45NyIgeDI9IjQ2MS41IiB5Mj0iMTA1Ljk3IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9IjAuNSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZmZiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJiIiB4MT0iMjQxLjAzIiB5MT0iNTkuOTUiIHgyPSI0NjEuNSIgeTI9IjU5Ljk1IiB4bGluazpocmVmPSIjYSIvPjwvZGVmcz48dGl0bGU+cGxhdGZvcm1zQXNzZXQgNDlxdWVzaXRvbnM8L3RpdGxlPjxwYXRoIGNsYXNzPSJhIiBkPSJNMjQyLjksNzYuNzcsMjU5LjcyLDYwbC02LjQ4LTYuNDhMMjQ3LDU5LjcxYTYuNDMsNi40MywwLDAsMS05LjA2LDBMMTc4LjI0LDBIMFYzMDkuMDVIMzA5LjA1VjE1MkwyNDIuOSw4NS44M0E2LjQzLDYuNDMsMCwwLDEsMjQyLjksNzYuNzdaIi8+PHBhdGggY2xhc3M9ImEiIGQ9Ik0yNDcsNTkuNzFsNi4yMy02LjI0TDIyNi4zMSwyNi41NGE2LjQyLDYuNDIsMCwwLDEsMC05TDI0My44LDBIMTc4LjI0TDIzOCw1OS43MUE2LjQzLDYuNDMsMCwwLDAsMjQ3LDU5LjcxWiIvPjxwYXRoIGNsYXNzPSJiIiBkPSJNMjQ3LDU5LjcxbDYuMjMtNi4yNEwyMjYuMzEsMjYuNTRhNi40Miw2LjQyLDAsMCwxLDAtOUwyNDMuOCwwSDE3OC4yNEwyMzgsNTkuNzFBNi40Myw2LjQzLDAsMCwwLDI0Nyw1OS43MVoiLz48cGF0aCBjbGFzcz0iYSIgZD0iTTI1OS43Miw2MCwyNDIuOSw3Ni43N2E2LjQzLDYuNDMsMCwwLDAsMCw5LjA2TDMwOS4wNSwxNTJ2LTQyLjdaIi8+PHBhdGggY2xhc3M9ImMiIGQ9Ik0yNTkuNzIsNjAsMjQyLjksNzYuNzdhNi40Myw2LjQzLDAsMCwwLDAsOS4wNkwzMDkuMDUsMTUydi00Mi43WiIvPjxwb2x5Z29uIGNsYXNzPSJhIiBwb2ludHM9IjMwOS4wNSAwIDMwNi43MiAwIDI1My4yNCA1My40NyAyNTkuNzIgNTkuOTUgMzA5LjA1IDEwLjYyIDMwOS4wNSAwIi8+PHBvbHlnb24gY2xhc3M9ImQiIHBvaW50cz0iMzA5LjA1IDAgMzA2LjcyIDAgMjUzLjI0IDUzLjQ3IDI1OS43MiA1OS45NSAzMDkuMDUgMTAuNjIgMzA5LjA1IDAiLz48cGF0aCBjbGFzcz0iYSIgZD0iTTI0My44LDAsMjI2LjMxLDE3LjQ5YTYuNDIsNi40MiwwLDAsMCwwLDkuMDVsMjYuOTMsMjYuOTNMMzA2LjcyLDBaIi8+PHBhdGggY2xhc3M9ImIiIGQ9Ik0yNDMuOCwwLDIyNi4zMSwxNy40OWE2LjQyLDYuNDIsMCwwLDAsMCw5LjA1bDI2LjkzLDI2LjkzTDMwNi43MiwwWiIvPjxwYXRoIGNsYXNzPSJkIiBkPSJNMjQzLjgsMCwyMjYuMzEsMTcuNDlhNi40Miw2LjQyLDAsMCwwLDAsOS4wNWwyNi45MywyNi45M0wzMDYuNzIsMFoiLz48cG9seWdvbiBjbGFzcz0iYSIgcG9pbnRzPSIyNTkuNzIgNTkuOTUgMzA5LjA1IDEwOS4yOCAzMDkuMDUgMTAuNjIgMjU5LjcyIDU5Ljk1Ii8+PHBvbHlnb24gY2xhc3M9ImUiIHBvaW50cz0iMjU5LjcyIDU5Ljk1IDMwOS4wNSAxMDkuMjggMzA5LjA1IDEwLjYyIDI1OS43MiA1OS45NSIvPjxwb2x5Z29uIGNsYXNzPSJkIiBwb2ludHM9IjI1OS43MiA1OS45NSAzMDkuMDUgMTA5LjI4IDMwOS4wNSAxMC42MiAyNTkuNzIgNTkuOTUiLz48L3N2Zz4=", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "horizontalAlignment": "Center", + "verticalContentAlignment": "Center", + "items": [ + { + "type": "Image", + "horizontalAlignment": "Center", + "url": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmlld0JveD0iMCAwIDk2IDk2IgogICBpZD0iSWNvbnNfSGVhZFdpdGhHZWFycyIKICAgb3ZlcmZsb3c9ImhpZGRlbiIKICAgdmVyc2lvbj0iMS4xIgogICBzb2RpcG9kaTpkb2NuYW1lPSJLbm93bGVkZ2VJY29uLnN2ZyIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC45Mi40ICg1ZGE2ODljMzEzLCAyMDE5LTAxLTE0KSI+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhMTMiPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxkZWZzCiAgICAgaWQ9ImRlZnMxMSIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgb2JqZWN0dG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBndWlkZXRvbGVyYW5jZT0iMTAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjE5MjAiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTE0NyIKICAgICBpZD0ibmFtZWR2aWV3OSIKICAgICBzaG93Z3JpZD0iZmFsc2UiCiAgICAgaW5rc2NhcGU6em9vbT0iMi40NTgzMzMzIgogICAgIGlua3NjYXBlOmN4PSI1LjI4ODEzNTYiCiAgICAgaW5rc2NhcGU6Y3k9IjQ4IgogICAgIGlua3NjYXBlOndpbmRvdy14PSItOCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTgiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJJY29uc19IZWFkV2l0aEdlYXJzIiAvPgogIDxwYXRoCiAgICAgZD0iTTQ3LjEgMTkuN0M0NC44IDE5LjcgNDIuOSAyMS42IDQyLjkgMjMuOSA0Mi45IDI2LjIgNDQuOCAyOC4xIDQ3LjEgMjguMSA0OS40IDI4LjEgNTEuMyAyNi4yIDUxLjMgMjMuOSA1MS4zIDIxLjYgNDkuNCAxOS43IDQ3LjEgMTkuN1oiCiAgICAgaWQ9InBhdGgyIgogICAgIHN0eWxlPSJmaWxsOiNmZmZmZmYiIC8+CiAgPGNpcmNsZQogICAgIGN4PSIzNC41IgogICAgIGN5PSI0NC4yIgogICAgIHI9IjQuMiIKICAgICBpZD0iY2lyY2xlNCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmIiAvPgogIDxwYXRoCiAgICAgZD0iTTU5IDI1LjMgNTYuNSAyNi41QzU2LjMgMjcuMyA1NS45IDI4IDU1LjUgMjguN0w1Ni40IDMxLjMgNTQuNCAzMy4zIDUxLjggMzIuNEM1MS4xIDMyLjggNTAuNCAzMy4xIDQ5LjYgMzMuM0w0OC40IDM1LjcgNDUuNiAzNS43IDQ0LjQgMzMuMkM0My42IDMzIDQyLjkgMzIuNyA0Mi4yIDMyLjNMMzkuNiAzMy4yIDM3LjYgMzEuMiAzOC41IDI4LjZDMzguMSAyNy45IDM3LjggMjcuMiAzNy42IDI2LjRMMzUuMSAyNS4yIDM1LjEgMjIuNCAzNy42IDIxLjJDMzcuOCAyMC40IDM4LjEgMTkuNyAzOC41IDE5TDM3LjcgMTYuNCAzOS43IDE0LjQgNDIuMyAxNS4zQzQzIDE0LjkgNDMuNyAxNC42IDQ0LjUgMTQuNEw0NS43IDExLjkgNDguNSAxMS45IDQ5LjcgMTQuM0M1MC41IDE0LjUgNTEuMiAxNC44IDUxLjkgMTUuMkw1NC41IDE0LjMgNTYuNSAxNi4zIDU1LjYgMTguOUM1NiAxOS42IDU2LjMgMjAuMyA1Ni41IDIxLjFMNTkgMjIuMyA1OSAyNS4zWk00Ni40IDQ1LjYgNDMuOSA0Ni44QzQzLjcgNDcuNiA0My40IDQ4LjMgNDMgNDlMNDMuOCA1MS42IDQxLjggNTMuNiAzOS4yIDUyLjdDMzguNSA1My4xIDM3LjggNTMuNCAzNyA1My42TDM1LjkgNTYgMzMuMSA1NiAzMS45IDUzLjVDMzEuMSA1My4zIDMwLjQgNTMgMjkuNyA1Mi42TDI3LjEgNTMuNCAyNS4xIDUxLjQgMjYgNDguOEMyNS42IDQ4LjEgMjUuMyA0Ny40IDI1LjEgNDYuNkwyMi42IDQ1LjQgMjIuNiA0Mi42IDI1LjEgNDEuNEMyNS4zIDQwLjYgMjUuNiAzOS45IDI2IDM5LjJMMjUuMSAzNi42IDI3LjEgMzQuNiAyOS43IDM1LjVDMzAuNCAzNS4xIDMxLjEgMzQuOCAzMS45IDM0LjZMMzMuMSAzMi4xIDM2IDMyLjEgMzcuMiAzNC42QzM4IDM0LjggMzguNyAzNS4xIDM5LjQgMzUuNUw0MiAzNC42IDQ0IDM2LjYgNDMuMSAzOS4yQzQzLjUgMzkuOSA0My44IDQwLjYgNDQgNDEuNEw0Ni41IDQyLjYgNDYuNCA0NS42IDQ2LjQgNDUuNlpNODEgNDkuMyA3NC4xIDM3LjMgNzQuMSAzNi44Qzc0LjUgMjUuOCA2OC45IDE1LjUgNTkuNCA5LjggNDkuOSA0LjIgMzguMiA0LjIgMjguNyA5LjggMTkuMiAxNS40IDEzLjYgMjUuOCAxNCAzNi44IDE0IDQ2LjMgMTguMyA1NS4yIDI1LjggNjFMMjUuOCA4Ni4zIDU3LjQgODYuMyA1Ny40IDc0LjMgNjIuMyA3NC4zQzY1LjUgNzQuMyA2OC41IDczIDcwLjcgNzAuOCA3Mi45IDY4LjUgNzQuMSA2NS41IDc0LjEgNjIuM0w3NC4xIDU2LjMgNzguNSA1Ni4zQzgxLjEgNTYgODMuNCA1MyA4MSA0OS4zWiIKICAgICBpZD0icGF0aDYiCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIgLz4KPC9zdmc+Cg==", + "width": "35px", + "height": "35px" + } + ], + "width": "auto" + }, + { + "type": "Column", + "verticalContentAlignment": "Center", + "items": [ + { + "type": "TextBlock", + "horizontalAlignment": "Left", + "size": "Medium", + "color": "Light", + "text": "{Title}", + "wrap": true + } + ], + "width": "stretch" + } + ] + }, + { + "type": "TextBlock", + "size": "Small", + "color": "Light", + "text": "{UpdatedTime}" + }, + { + "type": "TextBlock", + "size": "Small", + "color": "Light", + "text": "ID: {Id}" + } + ] + }, + { + "type": "TextBlock", + "wrap": true, + "text": "{Content}", + "maxLines": 5 + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.0", + "speak": "{Speak}" +} diff --git a/skills/src/csharp/experimental/itsmskill/Content/Ticket.1.0.json b/skills/src/csharp/experimental/itsmskill/Content/Ticket.1.0.json new file mode 100644 index 0000000000..0988fd6ba8 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Content/Ticket.1.0.json @@ -0,0 +1,81 @@ +{ + "type": "AdaptiveCard", + "id": "TicketCard", + "body": [ + { + "type": "Container", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "verticalContentAlignment": "Center", + "items": [ + { + "type": "TextBlock", + "horizontalAlignment": "Left", + "size": "Medium", + "color": "Default", + "text": "{Description}", + "wrap": true + } + ], + "width": "stretch" + } + ] + }, + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "items": [ + { + "type": "TextBlock", + "size": "Medium", + "color": "Default", + "text": "{UrgencyLevel}", + "weight": "Bolder" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "size": "Medium", + "color": "Default", + "text": "{State}" + } + ] + } + ] + }, + { + "type": "TextBlock", + "size": "Small", + "color": "Default", + "text": "{OpenedTime}" + }, + { + "type": "TextBlock", + "size": "Small", + "color": "Default", + "text": "ID: {Id}" + } + ] + }, + { + "type": "TextBlock", + "wrap": true, + "text": "{ResolvedReason}", + "maxLines": 5 + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.0", + "speak": "{Speak}" +} diff --git a/skills/src/csharp/experimental/itsmskill/Content/Ticket.json b/skills/src/csharp/experimental/itsmskill/Content/Ticket.json new file mode 100644 index 0000000000..5d202690d4 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Content/Ticket.json @@ -0,0 +1,96 @@ +{ + "type": "AdaptiveCard", + "id": "TicketCard", + "body": [ + { + "type": "Container", + "backgroundImage": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMzA5LjA1IDMwOS4wNSI+PGRlZnM+PHN0eWxlPi5he2ZpbGw6IzY1YWZlMjt9LmJ7ZmlsbDojMDA1OGE4O30uYiwuZHtvcGFjaXR5OjAuMjU7fS5jLC5le29wYWNpdHk6MC4yO30uY3tmaWxsOnVybCgjYSk7fS5ke2ZpbGw6I2I2ZGNmMTt9LmV7ZmlsbDp1cmwoI2IpO308L3N0eWxlPjxsaW5lYXJHcmFkaWVudCBpZD0iYSIgeDE9IjI0MS4wMyIgeTE9IjEwNS45NyIgeDI9IjQ2MS41IiB5Mj0iMTA1Ljk3IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9IjAuNSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZmZiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJiIiB4MT0iMjQxLjAzIiB5MT0iNTkuOTUiIHgyPSI0NjEuNSIgeTI9IjU5Ljk1IiB4bGluazpocmVmPSIjYSIvPjwvZGVmcz48dGl0bGU+cGxhdGZvcm1zQXNzZXQgNDlxdWVzaXRvbnM8L3RpdGxlPjxwYXRoIGNsYXNzPSJhIiBkPSJNMjQyLjksNzYuNzcsMjU5LjcyLDYwbC02LjQ4LTYuNDhMMjQ3LDU5LjcxYTYuNDMsNi40MywwLDAsMS05LjA2LDBMMTc4LjI0LDBIMFYzMDkuMDVIMzA5LjA1VjE1MkwyNDIuOSw4NS44M0E2LjQzLDYuNDMsMCwwLDEsMjQyLjksNzYuNzdaIi8+PHBhdGggY2xhc3M9ImEiIGQ9Ik0yNDcsNTkuNzFsNi4yMy02LjI0TDIyNi4zMSwyNi41NGE2LjQyLDYuNDIsMCwwLDEsMC05TDI0My44LDBIMTc4LjI0TDIzOCw1OS43MUE2LjQzLDYuNDMsMCwwLDAsMjQ3LDU5LjcxWiIvPjxwYXRoIGNsYXNzPSJiIiBkPSJNMjQ3LDU5LjcxbDYuMjMtNi4yNEwyMjYuMzEsMjYuNTRhNi40Miw2LjQyLDAsMCwxLDAtOUwyNDMuOCwwSDE3OC4yNEwyMzgsNTkuNzFBNi40Myw2LjQzLDAsMCwwLDI0Nyw1OS43MVoiLz48cGF0aCBjbGFzcz0iYSIgZD0iTTI1OS43Miw2MCwyNDIuOSw3Ni43N2E2LjQzLDYuNDMsMCwwLDAsMCw5LjA2TDMwOS4wNSwxNTJ2LTQyLjdaIi8+PHBhdGggY2xhc3M9ImMiIGQ9Ik0yNTkuNzIsNjAsMjQyLjksNzYuNzdhNi40Myw2LjQzLDAsMCwwLDAsOS4wNkwzMDkuMDUsMTUydi00Mi43WiIvPjxwb2x5Z29uIGNsYXNzPSJhIiBwb2ludHM9IjMwOS4wNSAwIDMwNi43MiAwIDI1My4yNCA1My40NyAyNTkuNzIgNTkuOTUgMzA5LjA1IDEwLjYyIDMwOS4wNSAwIi8+PHBvbHlnb24gY2xhc3M9ImQiIHBvaW50cz0iMzA5LjA1IDAgMzA2LjcyIDAgMjUzLjI0IDUzLjQ3IDI1OS43MiA1OS45NSAzMDkuMDUgMTAuNjIgMzA5LjA1IDAiLz48cGF0aCBjbGFzcz0iYSIgZD0iTTI0My44LDAsMjI2LjMxLDE3LjQ5YTYuNDIsNi40MiwwLDAsMCwwLDkuMDVsMjYuOTMsMjYuOTNMMzA2LjcyLDBaIi8+PHBhdGggY2xhc3M9ImIiIGQ9Ik0yNDMuOCwwLDIyNi4zMSwxNy40OWE2LjQyLDYuNDIsMCwwLDAsMCw5LjA1bDI2LjkzLDI2LjkzTDMwNi43MiwwWiIvPjxwYXRoIGNsYXNzPSJkIiBkPSJNMjQzLjgsMCwyMjYuMzEsMTcuNDlhNi40Miw2LjQyLDAsMCwwLDAsOS4wNWwyNi45MywyNi45M0wzMDYuNzIsMFoiLz48cG9seWdvbiBjbGFzcz0iYSIgcG9pbnRzPSIyNTkuNzIgNTkuOTUgMzA5LjA1IDEwOS4yOCAzMDkuMDUgMTAuNjIgMjU5LjcyIDU5Ljk1Ii8+PHBvbHlnb24gY2xhc3M9ImUiIHBvaW50cz0iMjU5LjcyIDU5Ljk1IDMwOS4wNSAxMDkuMjggMzA5LjA1IDEwLjYyIDI1OS43MiA1OS45NSIvPjxwb2x5Z29uIGNsYXNzPSJkIiBwb2ludHM9IjI1OS43MiA1OS45NSAzMDkuMDUgMTA5LjI4IDMwOS4wNSAxMC42MiAyNTkuNzIgNTkuOTUiLz48L3N2Zz4=", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "horizontalAlignment": "Center", + "verticalContentAlignment": "Center", + "items": [ + { + "type": "Image", + "horizontalAlignment": "Center", + "url": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmlld0JveD0iMCAwIDk2IDk2IgogICBpZD0iSWNvbnNfTGFiZWwiCiAgIG92ZXJmbG93PSJoaWRkZW4iCiAgIHZlcnNpb249IjEuMSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iUGljdHVyZTEuc3ZnIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjkyLjQgKDVkYTY4OWMzMTMsIDIwMTktMDEtMTQpIj4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE5Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZGVmcwogICAgIGlkPSJkZWZzNyIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgb2JqZWN0dG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBndWlkZXRvbGVyYW5jZT0iMTAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjE5MjAiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTE0NyIKICAgICBpZD0ibmFtZWR2aWV3NSIKICAgICBzaG93Z3JpZD0iZmFsc2UiCiAgICAgaW5rc2NhcGU6em9vbT0iNC45MTY2NjY3IgogICAgIGlua3NjYXBlOmN4PSIzMi43MzQ2MDkiCiAgICAgaW5rc2NhcGU6Y3k9IjYxLjUzOTI2MSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iLTgiCiAgICAgaW5rc2NhcGU6d2luZG93LXk9Ii04IgogICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjEiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0iSWNvbnNfTGFiZWwiIC8+CiAgPHBhdGgKICAgICBkPSJNNTAuNCA2OS40IDQ3LjYgNjYuNiA2Ni42IDQ3LjYgNjkuNCA1MC40IDUwLjQgNjkuNFpNNDAuNiA1OS42IDU5LjYgNDAuNiA2Mi40IDQzLjQgNDMuNCA2Mi40IDQwLjYgNTkuNlpNMzMuNiA1Mi42IDUyLjYgMzMuNiA1NS40IDM2LjQgMzYuNCA1NS40IDMzLjYgNTIuNlpNMjggMzJDMjUuOCAzMiAyNCAzMC4yIDI0IDI4IDI0IDI1LjggMjUuOCAyNCAyOCAyNCAzMC4yIDI0IDMyIDI1LjggMzIgMjggMzIgMzAuMiAzMC4yIDMyIDI4IDMyWk03Ny4yIDQ5LjIgNDkuMiAyMS4yQzQ4LjQgMjAuNCA0Ny40IDIwIDQ2LjQgMjBMMjggMjBDMjMuNiAyMCAyMCAyMy42IDIwIDI4TDIwIDQ2LjNDMjAgNDcuNCAyMC40IDQ4LjQgMjEuMiA0OS4xTDQ5LjIgNzcuMUM1MC44IDc4LjcgNTMuMyA3OC43IDU0LjkgNzcuMUw3Ny4yIDU0LjhDNzguNyA1My4zIDc4LjcgNTAuNyA3Ny4yIDQ5LjJaIgogICAgIGlkPSJwYXRoMiIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmIiAvPgogIDxwYXRoCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtzdHJva2Utd2lkdGg6MC4yMDMzODk4MiIKICAgICBkPSJNIDUwLjMyNzgyOSw3Ny43NzQ1MjEgQyA0OS40MzU4MDgsNzcuMzEwNTI2IDIwLjk4MzMyMiw0OC44NTc0OTUgMjAuNTE3NjM0LDQ3Ljk2Mzc1NyBjIC0wLjM3MzM0MiwtMC43MTY1MSAtMC4zOTMxOTUsLTEuMjY3Mzk5IC0wLjM5MzE5NSwtMTAuOTEwNzggMCwtOS41NTc5NTYgMC4wMjM3NiwtMTAuMjMyMzA2IDAuNDAzNDI0LC0xMS40NDg3OCAwLjcyMTcxLC0yLjMxMjQzNSAyLjYxMDIwNiwtNC4yMzczMzUgNC45NzEzOTQsLTUuMDY3MjI2IDEuMDU1MjQsLTAuMzcwODg3IDEuNzQ4MTk2LC0wLjM5NDg3NiAxMS40MDYyOTUsLTAuMzk0ODc2IDguODExNTMyLDAgMTAuMzkwOTk0LDAuMDQ0OTYgMTEuMDM4OTgsMC4zMTQyMzkgMC45MTYxNTksMC4zODA3MjIgMjguODk0MDYzLDI4LjIxNzY3NiAyOS42MzE2NDksMjkuNDgyMzcxIDAuNjUxOTMzLDEuMTE3ODI4IDAuNzc5NTcyLDEuOTA2MzAxIDAuNDk3NTY4LDMuMDczNjI3IC0wLjIyODg2MiwwLjk0NzM0MyAtMC43Nzc3OTMsMS41MzQxNDcgLTExLjY1NDA2NCwxMi40NTgxNTggLTYuMjc3NjM5LDYuMzA1MTk0IC0xMS44MjA3OTEsMTEuNzI4MDc2IC0xMi4zMTgxMTYsMTIuMDUwODQ3IC0wLjcyNzc2NywwLjQ3MjMzNSAtMS4xMjQ2ODEsMC41ODUyNzEgLTIuMDMzODk4LDAuNTc4NzE2IC0wLjYyMTMyLC0wLjAwNDUgLTEuNDA0MjQ4LC0wLjE1MDk2OSAtMS43Mzk4NDIsLTAuMzI1NTMyIHogbSA5LjcwODkyNCwtMTcuOTE3NjEzIDkuNTA3MTM4LC05LjUwNTUyNSAtMS4zNjUyODcsLTEuMzc1ODMxIGMgLTAuNzUwOTA5LC0wLjc1NjcwNiAtMS40NTg5MDMsLTEuMzc1ODMgLTEuNTczMzIyLC0xLjM3NTgzIC0wLjI2NTk1MiwwIC0xOS4wMjMyMTUsMTguNzQ0ODggLTE5LjAyMzIxNSwxOS4wMTA2NTYgMCwwLjI2NjE1NiAyLjQ3NDE4NCwyLjc1MjA1NiAyLjczOTA4MywyLjc1MjA1NiAwLjExNDY1NSwwIDQuNDg2Njc2LC00LjI3NzQ4NiA5LjcxNTYwMywtOS41MDU1MjYgeiBtIC03LjA2NTMyOCwtNi45Njg1NzkgOS41NTcyMjksLTkuNTU4ODUyIC0xLjQ3NDAzNSwtMS40NzQwMzUgLTEuNDc0MDM3LC0xLjQ3NDAzOCAtOS42MDY1NTUsOS42MDY1NTYgLTkuNjA2NTU2LDkuNjA2NTU3IDEuNDE1NDAyLDEuNDI2MzMgYyAwLjc3ODQ3MSwwLjc4NDQ4MyAxLjQ2Mzk4NCwxLjQyNjMzMiAxLjUyMzM2NCwxLjQyNjMzMiAwLjA1OTM4LDAgNC40MDg3MTQsLTQuMzAxNDgzIDkuNjY1MTg4LC05LjU1ODg1IHogbSAtNi45MTUyNTQsLTYuOTE1MjU0IDkuNTU3MjI5LC05LjU1ODg1MiAtMS40NzQwMzUsLTEuNDc0MDM2IC0xLjQ3NDAzNywtMS40NzQwMzYgLTkuNjA2NTU2LDkuNjA2NTU0IC05LjYwNjU1NSw5LjYwNjU1OCAxLjQxNTQwMiwxLjQyNjMzIGMgMC43Nzg0NzEsMC43ODQ0ODMgMS40NjM5ODUsMS40MjYzMzIgMS41MjMzNjQsMS40MjYzMzIgMC4wNTkzOCwwIDQuNDA4NzE0LC00LjMwMTQ4MyA5LjY2NTE4OCwtOS41NTg4NSB6IG0gLTE2LjkyNjE3MiwtMTQuMDQ2NiBjIDAuNDIyNTQ5LC0wLjExNzM0MyAxLjA5MDc4NCwtMC40OTY1MiAxLjQ4NDk2NiwtMC44NDI2MTcgMi45NDgzOSwtMi41ODg3MjEgMS4yMjcyMzcsLTcuMDc3MzU3IC0yLjcxMzc4OSwtNy4wNzczNTcgLTQuNTQ5OTM4LDAgLTUuNDQ2ODkxLDYuNjUxNDEgLTEuMDY0ODcyLDcuODk2NjIxIDAuOTU1NTY0LDAuMjcxNTM2IDEuMzg0Mjc1LDAuMjc1OTAxIDIuMjkzNjk1LDAuMDIzMzUgeiIKICAgICBpZD0icGF0aDM3MTgiCiAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KPC9zdmc+Cg==", + "width": "35px", + "height": "35px" + } + ], + "width": "auto" + }, + { + "type": "Column", + "verticalContentAlignment": "Center", + "items": [ + { + "type": "TextBlock", + "horizontalAlignment": "Left", + "size": "Medium", + "color": "Light", + "text": "{Description}", + "wrap": true + } + ], + "width": "stretch" + } + ] + }, + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "items": [ + { + "type": "TextBlock", + "size": "Medium", + "color": "Light", + "text": "{UrgencyLevel}", + "weight": "Bolder" + } + ] + }, + { + "type": "Column", + "items": [ + { + "type": "TextBlock", + "size": "Medium", + "color": "Light", + "text": "{State}" + } + ] + } + ] + }, + { + "type": "TextBlock", + "size": "Small", + "color": "Light", + "text": "{OpenedTime}" + }, + { + "type": "TextBlock", + "size": "Small", + "color": "Light", + "text": "ID: {Id}" + } + ] + }, + { + "type": "TextBlock", + "wrap": true, + "text": "{ResolvedReason}", + "maxLines": 5 + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.0", + "speak": "{Speak}" +} diff --git a/skills/src/csharp/experimental/itsmskill/Controllers/BotController.cs b/skills/src/csharp/experimental/itsmskill/Controllers/BotController.cs new file mode 100644 index 0000000000..6be6a25462 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Controllers/BotController.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Builder.Solutions; + +namespace ITSMSkill.Controllers +{ + [ApiController] + public class BotController : SkillController + { + public BotController( + IBot bot, + BotSettingsBase botSettings, + IBotFrameworkHttpAdapter botFrameworkHttpAdapter, + SkillWebSocketAdapter skillWebSocketAdapter) + : base(bot, botSettings, botFrameworkHttpAdapter, skillWebSocketAdapter) + { + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu new file mode 100644 index 0000000000..4e4c5be9aa --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu @@ -0,0 +1,20 @@ +[Ticket Create](./Ticket/Create.lu) + +# None +> from chitchat +- Good morning +- Hi +- Hello +- Heya +- Hi there! +- Hey +- Bye +- See you later +- Till we meet again +- Later +- Later alligator +- Goodbye + +> # Entity definitions + +$TicketDescription:simple diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Create.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Create.lu new file mode 100644 index 0000000000..038bd1a951 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Create.lu @@ -0,0 +1,20 @@ +# TicketCreate + +- add a ticket +- add a ticket about {TicketDescription=unable to connect} +- add a ticket for {TicketDescription=not detecting the device} +- add an incident +- open a ticket +- open a ticket about {TicketDescription=need oracle installed} +- open a ticket for {TicketDescription=need phone set up} +- open an incident about {TicketDescription=customer didn't receive fax} +- create a ticket +- create a ticket about {TicketDescription=missing my directory} +- create a ticket for {TicketDescription=new employee hire} +- create an incident for {TicketDescription=disk is still having issues} +- raise a ticket +- raise an incident +- raise a issue +- i would like to add a ticket +- i would like to open a ticket about {TicketDescription=lost connection} +- i would like to create a ticket for {TicketDescription=server down again} diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/general.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/general.lu new file mode 100644 index 0000000000..7ede276df9 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/general.lu @@ -0,0 +1,521 @@ +> # Intent definitions + +## Cancel +- cancel +- cancel app +- cancel cancel +- cancel it +- cancel never mind +- cancel that +- canceled +- cancelled +- do nothing +- don't do that +- don't do that anymore +- forget about it +- go away +- just cancel +- just cancel it +- nerver mind +- never mind +- never mind cancel +- never mind cancel that +- no cancel +- no cancel cancel +- no cancel it +- no cancel that +- no never mind +- no no cancel +- no no cancel it +- nothing never mind +- nothing please +- oh cancel +- oh don't do that +- please do nothing +- quit +- sorry, don't do it + + +## Confirm +- confirm +- do it +- fine +- go for it +- great +- i'm sure +- it's fine +- no doubt +- of course +- oh yeah +- oh yes +- ok +- ok for now +- okay +- perfect +- perfect thank you +- right +- right yes +- sounds good +- sounds good thank you +- sounds good thanks +- sounds good to me +- sounds great +- sounds perfect +- sure +- sure does +- sure is +- sure thing +- sure yes +- thank you very much +- thank you yes +- that is correct +- that is right +- that right +- that sounds good +- that's fine +- this is good +- very good +- ya +- yeah +- yeah baby +- yeah bro +- yeah buddy +- yeah cool +- yeah go ahead +- yeah go for it +- yeah good +- yes + + +## Escalate +- can i talk to a person +- contact support +- contact the customer service +- customer service +- human service +- i need manual customer service +- i need real human help +- i need support +- i want to talk to a human +- i want to talk to a real human +- is there any person i can talk to +- is there any real human +- is there any real person +- talk to a human + + +## FinishTask +- all finished +- all set +- done +- finish +- finished +- finished with +- i am done +- i am finished +- it finished +- it's done +- it's finished +- just be +- submit +- submit it + + +## GoBack +- back +- back please +- back to +- back to last +- back to last step +- get back +- get back please +- get back to last +- go back +- go back on +- go back please +- go back to +- last step +- no go back to +- no no go back to +- please return +- return + + +## Help +- any help +- can you help +- can you help me +- give me some help +- help +- how can i get it +- how to do it +- i need help +- i need some assist +- i need some help +- is there any help +- open help +- please help +- some help +- who can help me + +## Logout +- signout +- forget me +- sign out +- logout +- log out + +## None +- all of them +- i want them all +- i want to all of them + + +## ReadAloud +- can you read it +- can you read that +- can you read that for me +- can you you read page aloud +- could you tell me what that says +- detail aloud what that says +- hey read that for me +- i need to hear this page +- i would like you to read that for me +- make a reading of this page +- please read +- please read it +- please read me the page +- please read my latest email +- please read this +- please read this out loud +- please read this page +- please read this page aloud +- please read this page out loud +- please read this to me +- read all on the screen to me +- read aloud +- read aloud the current text onscreen +- read file aloud +- read it +- read it aloud +- read it out loud +- read it outloud +- read it please +- read it to me +- read me this page +- read my to list +- read outloud +- read page +- read page aloud +- read page outloud +- read sentence out loud +- read text +- read text aloud +- read that +- read that out loud +- read the page +- read the page onscreen to me +- read the page out loud +- read the page to me +- read the text to me +- read the words on this page +- read this for me please +- read this page +- read this page aloud +- read this page out loud +- read this page to me +- read to me +- read what is currently on the screen to me +- speak of what is on this page +- start reading this +- tell me about the information on the screen +- tell me the current text on screen +- vocalize what s on the page +- what does the page say +- would you please read that for me +- would you read that out loud please +- would you read that to me please + + +## Reject +- i don't like it +- i reject +- negative +- never +- no +- no i don't want that +- no later +- no leave it +- no more no +- no no +- no no no +- no no thank you +- no not that one +- no reject it +- no thank you +- no thanks +- no way +- no wrong +- nope +- not +- not at all +- not even close +- not exactly +- not now +- not quite +- not right now +- not that +- nothing much +- oh no +- reject +- reject it + + +## Repeat +- again +- could you say it again +- i didn't hear repeat again +- i have not heard +- pardon +- repeat +- repeat please +- repeat that +- say again +- say again please +- say that again +- sorry +- what +- what did you say +- what was that again + + +## SelectAny +- any of it +- any one is ok +- anyone is fine +- anything +- choose anyone +- choose one of it randomly +- opt for a random one +- opt for any of it +- select a random choice +- select a random one +- select any +- select any choice +- select any of it + + +## SelectItem +- choose last +- choose last one +- choose no.2 +- choose the {DirectionalReference=bottom left} +- choose the first choice +- choose the fourth one +- choose the {DirectionalReference=upper left} choice +- choose the {DirectionalReference=upper right} one +- choose {DirectionalReference=top right} +- choose {DirectionalReference=top right} one +- i like {DirectionalReference=left} one +- i like second +- i like second one +- i like the {DirectionalReference=bottom} one +- i like the first one +- i like the third +- i like the third choice +- i like the {DirectionalReference=top right} one +- i like the {DirectionalReference=upper right} +- i like {DirectionalReference=upper right} +- i want {DirectionalReference=bottom} +- i want fourth +- i want {DirectionalReference=left} +- i want {DirectionalReference=right} one +- i want the first +- i want the fourth choice +- i want the {DirectionalReference=left} +- i want the {DirectionalReference=lower} choice +- i want the {DirectionalReference=right} one +- i want the second one +- i want third one +- i want to choose {DirectionalReference=bottom} one +- i want to choose {DirectionalReference=lower right} +- i want to choose {DirectionalReference=right} +- i want to choose second one +- i want to choose the first one +- i want to choose the fourth +- i want to choose the last choice +- i want to choose the {DirectionalReference=left} one +- i want to choose the {DirectionalReference=lower} choice +- i want to choose the {DirectionalReference=right} +- i want to choose third +- opt for first one +- opt for last +- opt for {DirectionalReference=left} +- opt for {DirectionalReference=right} one +- opt for the last one +- opt for the {DirectionalReference=left} one +- opt for the {DirectionalReference=lower} choice +- opt for the {DirectionalReference=right} +- opt for the second +- opt for the second choice +- select fourth one +- select {DirectionalReference=lower} one +- select no.5 +- select the {DirectionalReference=bottom} choice +- select the first +- select the last choice +- select the {DirectionalReference=lower} one +- select the {DirectionalReference=right} one +- select the third one +- select the {DirectionalReference=upper right} +- select third +- select {DirectionalReference=upper} +- what about the last +- what about the third one + + +## SelectNone +- i don't want to choose any one +- i don't want to select any one +- i want neither of them +- i want none of them +- neither +- neither of those +- neither one +- neither one of them +- neither thank you +- none +- none none +- none none of them +- none of them +- none of them thank you +- none of these +- none of those +- they look bad, can you give me other choices + + +## ShowNext +- and after that +- display more +- displays more +- give me more +- go forward +- go to the next one +- go to the next three items +- i need to go to the next one +- i want more +- more +- move to the next one +- next +- reveal more +- show me the next +- show more +- show the next 3 +- show the next 4 items +- show the next one +- show the next two options +- tell me more +- tell more +- what about next one +- what after that +- what's after that +- what's more +- what's next +- what's the next 2 +- what's up next + + +## ShowPrevious +- back to the last one +- bring the previous one +- display previously +- get back to the last one +- go back to last one +- go back to previous +- go back to the last one +- go previously +- go to the previous +- go to the previous one +- previous one +- previous one please +- return to the previous one +- reveal previous +- reveal previously +- show earlier +- show me the previous one +- show previous +- show the previous one +- what before that +- what is the previous +- what's before that + + +## StartOver +- clear and start again +- could you start it over +- please begin again +- restart +- restart it +- start again +- start it over +- start over +- start over it +- turn over a new leaf + + +## Stop +- baby just be quiet +- be quiet +- be quiet now +- come on stop +- dismiss +- end +- end it +- exit exit +- exit stop +- god shut up +- hey stop +- i love you to stop talking +- i mean stop listening +- i said stop +- just be quiet +- my god shut up +- never mind stop +- no be quiet +- no be quiet now +- no no no no stop talking +- no shut up +- no stop +- nobody cares stop talking +- nowhere just be quiet +- oh my god shut up +- ok stop stop +- quiet +- quiet now +- shut the fuck up +- shut up +- shut up be quiet +- shut up quiet +- shut your mouth +- stop please +- stop talking +- turn off +- turn off stop + + +> # Entity definitions + +$DirectionalReference:simple + + +> # PREBUILT Entity definitions + +$PREBUILT:number + +$PREBUILT:ordinal + + +> # Phrase list definitions + + +> # List entities diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/parameters.template.json b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/parameters.template.json new file mode 100644 index 0000000000..95841eb589 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/parameters.template.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "useCosmosDb": { + "value": false + }, + "useStorage": { + "value": false + }, + "appServicePlanSku": { + "value": { + "tier": "Free", + "name": "F1" + } + }, + "botServiceSku": { + "value": "F0" + }, + "luisServiceSku": { + "value": "F0" + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/template.json b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/template.json new file mode 100644 index 0000000000..5eb8a2cab3 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/template.json @@ -0,0 +1,245 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "type": "string", + "defaultValue": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "suffix": { + "type": "string", + "defaultValue": "[take(uniqueString(resourceGroup().id), 7)]" + }, + "microsoftAppId": { + "type": "string" + }, + "microsoftAppPassword": { + "type": "string" + }, + "useCosmosDb": { + "type": "bool", + "defaultValue": true + }, + "cosmosDbName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-', parameters('suffix'))]" + }, + "useStorage": { + "type": "bool", + "defaultValue": true + }, + "storageAccountName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-', parameters('suffix'))]" + }, + "appServicePlanName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-', parameters('suffix'))]" + }, + "appServicePlanSku": { + "type": "object", + "defaultValue": { + "tier": "Standard", + "name": "S1" + } + }, + "appInsightsName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-', parameters('suffix'))]" + }, + "appInsightsLocation": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "botWebAppName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-', parameters('suffix'))]" + }, + "botServiceName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-', parameters('suffix'))]" + }, + "botServiceSku": { + "type": "string", + "defaultValue": "S1" + }, + "luisServiceName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-luis-', parameters('suffix'))]" + }, + "luisServiceSku": { + "type": "string", + "defaultValue": "S0" + }, + "luisServiceLocation": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + } + }, + "variables": { + "botWebAppName": "[replace(parameters('botWebAppName'), '_', '')]", + "storageAccountName": "[toLower(take(replace(replace(parameters('storageAccountName'), '-', ''), '_', ''), 24))]", + "cosmosDbAccountName": "[toLower(take(replace(parameters('cosmosDbName'), '_', ''), 31))]", + "botEndpoint": "[concat('https://', toLower(variables('botWebAppName')), '.azurewebsites.net/api/messages')]" + }, + "resources": [ + { + "apiVersion": "2018-02-01", + "name": "99ea37e6-a3e6-4102-a249-71c880607386", + "type": "Microsoft.Resources/deployments", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [] + } + } + }, + { + "comments": "CosmosDB for bot state.", + "type": "Microsoft.DocumentDB/databaseAccounts", + "kind": "GlobalDocumentDB", + "apiVersion": "2015-04-08", + "name": "[variables('cosmosDbAccountName')]", + "location": "[parameters('location')]", + "properties": { + "databaseAccountOfferType": "Standard", + "locations": [ + { + "locationName": "[parameters('location')]", + "failoverPriority": 0 + } + ] + }, + "condition": "[parameters('useCosmosDb')]" + }, + { + "comments": "storage account", + "type": "Microsoft.Storage/storageAccounts", + "kind": "StorageV2", + "apiVersion": "2018-07-01", + "name": "[variables('storageAccountName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_LRS" + }, + "condition": "[parameters('useStorage')]" + }, + { + "comments": "app service plan", + "type": "Microsoft.Web/serverFarms", + "apiVersion": "2018-02-01", + "name": "[parameters('appServicePlanName')]", + "location": "[parameters('location')]", + "sku": "[parameters('appServicePlanSku')]", + "properties": {} + }, + { + "comments": "app insights", + "type": "Microsoft.Insights/components", + "kind": "web", + "apiVersion": "2015-05-01", + "name": "[parameters('appInsightsName')]", + "location": "[parameters('appInsightsLocation')]", + "properties": { + "Application_Type": "web" + } + }, + { + "comments": "bot web app", + "type": "Microsoft.Web/sites", + "apiVersion": "2018-02-01", + "name": "[variables('botWebAppName')]", + "location": "[parameters('location')]", + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]", + "siteConfig": { + "webSocketsEnabled": true, + "appSettings": [ + { + "name": "MicrosoftAppId", + "value": "[parameters('microsoftAppId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('microsoftAppPassword')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]" + ] + }, + { + "comments": "bot service", + "type": "Microsoft.BotService/botServices", + "kind": "sdk", + "apiVersion": "2018-07-12", + "name": "[parameters('botServiceName')]", + "location": "global", + "sku": { + "name": "[parameters('botServiceSku')]" + }, + "properties": { + "displayName": "[parameters('botServiceName')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('microsoftAppId')]", + "developerAppInsightKey": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName'))).InstrumentationKey]", + "developerAppInsightsApplicationId": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName'))).ApplicationId]" + } + }, + { + "comments": "Cognitive service key for all LUIS apps.", + "type": "Microsoft.CognitiveServices/accounts", + "kind": "LUIS", + "apiVersion": "2017-04-18", + "name": "[parameters('luisServiceName')]", + "location": "[parameters('luisServiceLocation')]", + "sku": { + "name": "[parameters('luisServiceSku')]" + } + } + ], + "outputs": { + "botWebAppName": { + "type": "string", + "value": "[variables('botWebAppName')]" + }, + "ApplicationInsights": { + "type": "object", + "value": { + "InstrumentationKey": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName'))).InstrumentationKey]" + } + }, + "blobStorage": { + "type": "object", + "value": { + "connectionString": "[if(parameters('useStorage'), concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2018-07-01').keys[0].value, ';EndpointSuffix=core.windows.net'), '')]", + "container": "transcripts" + } + }, + "cosmosDb": { + "type": "object", + "value": { + "cosmosDBEndpoint": "[if(parameters('useCosmosDb'), reference(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmosDbAccountName'))).documentEndpoint, '')]", + "authKey": "[if(parameters('useCosmosDb'), listKeys(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmosDbAccountName')), '2015-04-08').primaryMasterKey, '')]", + "databaseId": "botstate-db", + "collectionId": "botstate-collection" + } + }, + "luis": { + "type": "object", + "value": { + "accountName": "[parameters('luisServiceName')]", + "region": "[parameters('luisServiceLocation')]", + "key": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', parameters('luisServiceName')),'2017-04-18').key1]" + } + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/deploy.ps1 b/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/deploy.ps1 new file mode 100644 index 0000000000..2ec874cf98 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/deploy.ps1 @@ -0,0 +1,245 @@ +#Requires -Version 6 + +Param( + [string] $name, + [string] $resourceGroup, + [string] $location, + [string] $appId, + [string] $appPassword, + [string] $luisAuthoringKey, + [string] $luisAuthoringRegion, + [string] $parametersFile, + [string] $languages = "en-us", + [string] $projDir = $(Get-Location), + [string] $logFile = $(Join-Path $PSScriptRoot .. "deploy_log.txt") +) + +# Reset log file +if (Test-Path $logFile) { + Clear-Content $logFile -Force | Out-Null +} +else { + New-Item -Path $logFile | Out-Null +} + +if (-not (Test-Path (Join-Path $projDir 'appsettings.json'))) +{ + Write-Host "! Could not find an 'appsettings.json' file in the current directory." -ForegroundColor DarkRed + Write-Host "+ Please re-run this script from your project directory." -ForegroundColor Magenta + Break +} + +# Get mandatory parameters +if (-not $name) { + $name = Read-Host "? Bot Name (used as default name for resource group and deployed resources)" +} + +if (-not $resourceGroup) { + $resourceGroup = $name +} + +if (-not $location) { + $location = Read-Host "? Azure resource group region" +} + +if (-not $appPassword) { + $appPassword = Read-Host "? Password for MSA app registration (must be at least 16 characters long, contain at least 1 special character, and contain at least 1 numeric character)" +} + +if (-not $luisAuthoringRegion) { + $luisAuthoringRegion = Read-Host "? LUIS Authoring Region (westus, westeurope, or australiaeast)" +} + +if (-not $luisAuthoringKey) { + Switch ($luisAuthoringRegion) { + "westus" { + $luisAuthoringKey = Read-Host "? LUIS Authoring Key (found at https://luis.ai/user/settings)" + Break + } + "westeurope" { + $luisAuthoringKey = Read-Host "? LUIS Authoring Key (found at https://eu.luis.ai/user/settings)" + Break + } + "australiaeast" { + $luisAuthoringKey = Read-Host "? LUIS Authoring Key (found at https://au.luis.ai/user/settings)" + Break + } + default { + Write-Host "! $($luisAuthoringRegion) is not a valid LUIS authoring region." -ForegroundColor DarkRed + Break + } + } + + if (-not $luisAuthoringKey) { + Break + } +} + +if (-not $appId) { + # Create app registration + $app = (az ad app create ` + --display-name $name ` + --password `"$($appPassword)`" ` + --available-to-other-tenants ` + --reply-urls 'https://token.botframework.com/.auth/web/redirect' ` + --output json) + + # Retrieve AppId + if ($app) { + $appId = ($app | ConvertFrom-Json) | Select-Object -ExpandProperty appId + } + + if(-not $appId) { + Write-Host "! Could not provision Microsoft App Registration automatically. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + Write-Host "+ Provision an app manually in the Azure Portal, then try again providing the -appId and -appPassword arguments. See https://aka.ms/vamanualappcreation for more information." -ForegroundColor Magenta + Break + } +} + +# Get timestamp +$timestamp = Get-Date -f MMddyyyyHHmmss + +# Create resource group +Write-Host "> Creating resource group ..." +(az group create --name $resourceGroup --location $location --output json) 2>> $logFile | Out-Null + +# Deploy Azure services (deploys LUIS, QnA Maker, Content Moderator, CosmosDB) +if ($parametersFile) { + Write-Host "> Validating Azure deployment ..." + $validation = az group deployment validate ` + --resource-group $resourcegroup ` + --template-file "$(Join-Path $PSScriptRoot '..' 'Resources' 'template.json')" ` + --parameters "@$($parametersFile)" ` + --parameters name=$name microsoftAppId=$appId microsoftAppPassword="`"$($appPassword)`"" ` + --output json + + if ($validation) { + $validation >> $logFile + $validation = $validation | ConvertFrom-Json + + if (-not $validation.error) { + Write-Host "> Deploying Azure services (this could take a while)..." -ForegroundColor Yellow + $deployment = az group deployment create ` + --name $timestamp ` + --resource-group $resourceGroup ` + --template-file "$(Join-Path $PSScriptRoot '..' 'Resources' 'template.json')" ` + --parameters "@$($parametersFile)" ` + --parameters name=$name microsoftAppId=$appId microsoftAppPassword="`"$($appPassword)`"" ` + --output json + } + else { + Write-Host "! Template is not valid with provided parameters. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Error: $($validation.error.message)" -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + Write-Host "+ To delete this resource group, run 'az group delete -g $($resourceGroup) --no-wait'" -ForegroundColor Magenta + Break + } + } +} +else { + Write-Host "> Validating Azure deployment ..." + $validation = az group deployment validate ` + --resource-group $resourcegroup ` + --template-file "$(Join-Path $PSScriptRoot '..' 'Resources' 'template.json')" ` + --parameters name=$name microsoftAppId=$appId microsoftAppPassword="`"$($appPassword)`"" ` + --output json + + if ($validation) { + $validation >> $logFile + $validation = $validation | ConvertFrom-Json + + if (-not $validation.error) { + Write-Host "> Deploying Azure services (this could take a while)..." -ForegroundColor Yellow + $deployment = az group deployment create ` + --name $timestamp ` + --resource-group $resourceGroup ` + --template-file "$(Join-Path $PSScriptRoot '..' 'Resources' 'template.json')" ` + --parameters name=$name microsoftAppId=$appId microsoftAppPassword="`"$($appPassword)`"" ` + --output json + } + else { + Write-Host "! Template is not valid with provided parameters. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Error: $($validation.error.message)" -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + Write-Host "+ To delete this resource group, run 'az group delete -g $($resourceGroup) --no-wait'" -ForegroundColor Magenta + Break + } + } +} + +# Get deployment outputs +$outputs = (az group deployment show ` + --name $timestamp ` + --resource-group $resourceGroup ` + --query properties.outputs ` + --output json) 2>> $logFile + +# If it succeeded then we perform the remainder of the steps +if ($outputs) +{ + # Log and convert to JSON + $outputs >> $logFile + $outputs = $outputs | ConvertFrom-Json + $outputMap = @{} + $outputs.PSObject.Properties | Foreach-Object { $outputMap[$_.Name] = $_.Value } + + # Update appsettings.json + Write-Host "> Updating appsettings.json ..." + if (Test-Path $(Join-Path $projDir appsettings.json)) { + $settings = Get-Content $(Join-Path $projDir appsettings.json) | ConvertFrom-Json + } + else { + $settings = New-Object PSObject + } + + $settings | Add-Member -Type NoteProperty -Force -Name 'microsoftAppId' -Value $appId + $settings | Add-Member -Type NoteProperty -Force -Name 'microsoftAppPassword' -Value $appPassword + foreach ($key in $outputMap.Keys) { $settings | Add-Member -Type NoteProperty -Force -Name $key -Value $outputMap[$key].value } + $settings | ConvertTo-Json -depth 100 | Out-File $(Join-Path $projDir appsettings.json) + + if ($outputs.qnaMaker.value.key) { $qnaSubscriptionKey = $outputs.qnaMaker.value.key } + + # Delay to let QnA Maker finish setting up + Start-Sleep -s 30 + + # Deploy cognitive models + Invoke-Expression "& '$(Join-Path $PSScriptRoot 'deploy_cognitive_models.ps1')' -name $($name) -luisAuthoringRegion $($luisAuthoringRegion) -luisAuthoringKey $($luisAuthoringKey) -luisAccountName $($outputs.luis.value.accountName) -luisAccountRegion $($outputs.luis.value.region) -luisSubscriptionKey $($outputs.luis.value.key) -resourceGroup $($resourceGroup) -qnaSubscriptionKey '$($qnaSubscriptionKey)' -outFolder '$($projDir)' -languages '$($languages)'" + + # Publish bot + Invoke-Expression "& '$(Join-Path $PSScriptRoot 'publish.ps1')' -name $($outputs.botWebAppName.value) -resourceGroup $($resourceGroup) -projFolder '$($projDir)'" + + Write-Host "> Done." +} +else +{ + # Check for failed deployments + $operations = (az group deployment operation list -g $resourceGroup -n $timestamp --output json) 2>> $logFile | Out-Null + + if ($operations) { + $operations = $operations | ConvertFrom-Json + $failedOperations = $operations | Where { $_.properties.statusmessage.error -ne $null } + if ($failedOperations) { + foreach ($operation in $failedOperations) { + switch ($operation.properties.statusmessage.error.code) { + "MissingRegistrationForLocation" { + Write-Host "! Deployment failed for resource of type $($operation.properties.targetResource.resourceType). This resource is not avaliable in the location provided." -ForegroundColor DarkRed + Write-Host "+ Update the .\Deployment\Resources\parameters.template.json file with a valid region for this resource and provide the file path in the -parametersFile parameter." -ForegroundColor Magenta + } + default { + Write-Host "! Deployment failed for resource of type $($operation.properties.targetResource.resourceType)." + Write-Host "! Code: $($operation.properties.statusMessage.error.code)." + Write-Host "! Message: $($operation.properties.statusMessage.error.message)." + } + } + } + } + } + else { + Write-Host "! Deployment failed. Please refer to the log file for more information." -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + } + + Write-Host "+ To delete this resource group, run 'az group delete -g $($resourceGroup) --no-wait'" -ForegroundColor Magenta + Break +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/deploy_cognitive_models.ps1 b/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/deploy_cognitive_models.ps1 new file mode 100644 index 0000000000..7c9a676ad5 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/deploy_cognitive_models.ps1 @@ -0,0 +1,300 @@ +#Requires -Version 6 + +Param( + [string] $name, + [string] $luisAuthoringRegion, + [string] $luisAuthoringKey, + [string] $luisAccountName, + [string] $luisAccountRegion, + [string] $luisSubscriptionKey, + [string] $qnaSubscriptionKey, + [string] $resourceGroup, + [switch] $useDispatch = $true, + [string] $languages = "en-us", + [string] $outFolder = $(Get-Location), + [string] $logFile = $(Join-Path $PSScriptRoot .. "deploy_cognitive_models_log.txt") +) + +. $PSScriptRoot\luis_functions.ps1 +. $PSScriptRoot\qna_functions.ps1 + +# Reset log file +if (Test-Path $logFile) { + Clear-Content $logFile -Force | Out-Null +} +else { + New-Item -Path $logFile | Out-Null +} + +# Get mandatory parameters +if (-not $name) { + $name = Read-Host "? Base name for Cognitive Models" +} + +if (-not $luisAuthoringRegion) { + $luisAuthoringRegion = Read-Host "? LUIS Authoring Region (westus, westeurope, or australiaeast)" +} + +if (-not $luisAuthoringKey) { + Switch ($luisAuthoringRegion) { + "westus" { + $luisAuthoringKey = Read-Host "? LUIS Authoring Key (found at https://luis.ai/user/settings)" + Break + } + "westeurope" { + $luisAuthoringKey = Read-Host "? LUIS Authoring Key (found at https://eu.luis.ai/user/settings)" + Break + } + "australiaeast" { + $luisAuthoringKey = Read-Host "? LUIS Authoring Key (found at https://au.luis.ai/user/settings)" + Break + } + default { + Write-Host "! $($luisAuthoringRegion) is not a valid LUIS authoring region." -ForegroundColor DarkRed + Break + } + } + + if (-not $luisAuthoringKey) { + Break + } +} + +if (-not $luisAccountName) { + $luisAccountName = Read-Host "? LUIS Service Name (exising service in Azure required)" +} + +if (-not $resourceGroup) { + $resourceGroup = $name + + $rgExists = az group exists -n $resourceGroup --output json + if ($rgExists -eq "false") + { + $resourceGroup = Read-Host "? LUIS Service Resource Group (exising service in Azure required)" + } +} + +if (-not $luisSubscriptionKey) { + $keys = az cognitiveservices account keys list --name $luisAccountName --resource-group $resourceGroup --output json | ConvertFrom-Json + + if ($keys) { + $luisSubscriptionKey = $keys.key1 + } + else { + Write-Host "! Could not retrieve LUIS Subscription Key." -ForgroundColor DarkRed + Write-Host "+ Verify the -luisAccountName and -resourceGroup parameters are correct." -ForegroundColor Magenta + Break + } +} + +if (-not $luisAccountRegion) { + $luisAccountRegion = Read-Host "? LUIS Service Location" +} + +if (-not $qnaSubscriptionKey) { + $useQna = $false +} +else { + $useQna = $true +} + +$azAccount = az account show --output json | ConvertFrom-Json +$azAccessToken = $(Invoke-Expression "az account get-access-token --output json") | ConvertFrom-Json + +# Get languages +$languageArr = $languages -split "," + +# Initialize settings obj +$settings = @{ defaultLocale = $languageArr[0]; cognitiveModels = New-Object PSObject } + +# Deploy localized resources +Write-Host "> Deploying cognitive models ..." +foreach ($language in $languageArr) +{ + $langCode = ($language -split "-")[0] + $config = New-Object PSObject + + if ($useDispatch) { + # Add dispatch to config + $config | Add-Member -MemberType NoteProperty -Name dispatchModel -Value $(New-Object PSObject) + + # Initialize Dispatch + Write-Host "> Initializing dispatch model ..." + $dispatchName = "$($name)$($langCode)_Dispatch" + $dataFolder = Join-Path $PSScriptRoot .. Resources Dispatch $langCode + (dispatch init ` + --name $dispatchName ` + --luisAuthoringKey $luisAuthoringKey ` + --luisAuthoringRegion $luisAuthoringRegion ` + --dataFolder $dataFolder) 2>> $logFile | Out-Null + } + + # Deploy LUIS apps + $luisFiles = Get-ChildItem "$(Join-Path $PSScriptRoot .. 'Resources' 'LU' $langCode)" | Where {$_.extension -eq ".lu"} + if ($luisFiles) { + $config | Add-Member -MemberType NoteProperty -Name languageModels -Value @() + + foreach ($lu in $luisFiles) + { + # Deploy LUIS model + $luisApp = DeployLUIS ` + -name $name ` + -lu_file $lu ` + -region $luisAuthoringRegion ` + -luisAuthoringKey $luisAuthoringKey ` + -language $language ` + -log $logFile + + Write-Host "> Setting LUIS subscription key ..." + if ($luisApp) { + # Setting subscription key + $addKeyResult = luis add appazureaccount ` + --appId $luisApp.id ` + --authoringKey $luisAuthoringKey ` + --region $luisAuthoringRegion ` + --accountName $luisAccountName ` + --azureSubscriptionId $azAccount.id ` + --resourceGroup $resourceGroup ` + --armToken "$($azAccessToken.accessToken)" 2>> $logFile + + if (-not $addKeyResult) { + $luisKeySet = $false + Write-Host "! Could not assign subscription key automatically. Review the log for more information. " -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + Write-Host "+ Please assign your subscription key manually in the LUIS portal." -ForegroundColor Magenta + } + + if ($useDispatch) { + # Add luis app to dispatch + Write-Host "> Adding $($lu.BaseName) app to dispatch model ..." + (dispatch add ` + --type "luis" ` + --name $luisApp.name ` + --id $luisApp.id ` + --region $luisApp.region ` + --intentName "l_$($lu.BaseName)" ` + --dataFolder $dataFolder ` + --dispatch "$(Join-Path $dataFolder "$($dispatchName).dispatch")") 2>> $logFile | Out-Null + } + + # Add to config + $config.languageModels += @{ + id = $lu.BaseName + name = $luisApp.name + appid = $luisApp.id + authoringkey = $luisAuthoringKey + authoringRegion = $luisAuthoringRegion + subscriptionkey = $luisSubscriptionKey + version = $luisApp.activeVersion + region = $luisAccountRegion + } + } + else { + Write-Host "! Could not create LUIS app. Skipping dispatch add." -ForegroundColor Cyan + } + } + } + + if ($useQna) { + if (Test-Path $(Join-Path $PSScriptRoot .. 'Resources' 'QnA' $langCode)) { + # Deploy QnA Maker KBs + $qnaFiles = Get-ChildItem "$(Join-Path $PSScriptRoot .. 'Resources' 'QnA' $langCode)" -Recurse | Where {$_.extension -eq ".lu"} + + if ($qnaFiles) { + $config | Add-Member -MemberType NoteProperty -Name knowledgebases -Value @() + + foreach ($lu in $qnaFiles) + { + # Deploy QnA Knowledgebase + $qnaKb = DeployKB -name $name -lu_file $lu -qnaSubscriptionKey $qnaSubscriptionKey -log $logFile + + if ($qnaKb) { + if ($useDispatch) { + Write-Host "> Adding $($lu.BaseName) kb to dispatch model ..." + (dispatch add ` + --type "qna" ` + --name $qnaKb.name ` + --id $qnaKb.id ` + --key $qnaSubscriptionKey ` + --intentName "q_$($lu.BaseName)" ` + --dataFolder $dataFolder ` + --dispatch "$(Join-Path $dataFolder "$($dispatchName).dispatch")") 2>> $logFile | Out-Null + } + + # Add to config + $config.knowledgebases += @{ + id = $lu.BaseName + name = $qnaKb.name + kbId = $qnaKb.kbId + subscriptionKey = $qnaKb.subscriptionKey + endpointKey = $qnaKb.endpointKey + hostname = $qnaKb.hostname + } + } + else { + Write-Host "! Could not create knowledgebase. Skipping dispatch add." -ForegroundColor Cyan + } + } + } + } + else { + Write-Host "! No knowledgebases found. Skipping." -ForegroundColor Cyan + } + } + else { + Write-Host "! No QnA Maker Subscription Key provided. Skipping knowledgebases." -ForegroundColor Cyan + } + + if ($useDispatch) { + # Create dispatch model + Write-Host "> Creating dispatch model..." + $dispatch = (dispatch create ` + --dispatch "$(Join-Path $dataFolder "$($dispatchName).dispatch")" ` + --dataFolder $dataFolder ` + --culture $language) 2>> $logFile + + if (-not $dispatch) { + Write-Host "! Could not create Dispatch app. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + Break + } + else { + $dispatchApp = $dispatch | ConvertFrom-Json + + # Setting subscription key + Write-Host "> Setting LUIS subscription key ..." + $addKeyResult = luis add appazureaccount ` + --appId $dispatchApp.appId ` + --accountName $luisAccountName ` + --authoringKey $luisAuthoringKey ` + --region $luisAuthoringRegion ` + --azureSubscriptionId $azAccount.id ` + --resourceGroup $resourceGroup ` + --armToken $azAccessToken.accessToken 2>> $logFile + + if (-not $addKeyResult) { + $luisKeySet = $false + Write-Host "! Could not assign subscription key automatically. Review the log for more information. " -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + Write-Host "+ Please assign your subscription key manually in the LUIS portal." -ForegroundColor Magenta + } + + # Add to config + $config.dispatchModel = @{ + type = "dispatch" + name = $dispatchApp.name + appid = $dispatchApp.appId + authoringkey = $luisauthoringkey + authoringRegion = $luisAuthoringRegion + subscriptionkey = $luisSubscriptionKey + region = $luisAccountRegion + } + } + } + + # Add config to cognitivemodels dictionary + $settings.cognitiveModels | Add-Member -Type NoteProperty -Force -Name $langCode -Value $config +} + +# Write out config to file +$settings | ConvertTo-Json -depth 100 | Out-File $(Join-Path $outFolder "cognitivemodels.json" ) \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/luis_functions.ps1 b/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/luis_functions.ps1 new file mode 100644 index 0000000000..0da139daa8 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/luis_functions.ps1 @@ -0,0 +1,112 @@ +function DeployLUIS ($name, $lu_file, $region, $luisAuthoringKey, $language, $log) +{ + $id = $lu_file.BaseName + $outFile = "$($id).luis" + $outFolder = $lu_file.DirectoryName + $appName = "$($name)$($langCode)_$($id)" + + # Parse LU file + Write-Host "> Parsing $($id) LU file ..." + ludown parse toluis ` + --in $lu_file ` + --luis_culture $language ` + --out_folder $outFolder ` + --out $outFile + + # Create LUIS app + Write-Host "> Deploying $($id) LUIS app ..." + $luisApp = (luis import application ` + --appName $appName ` + --authoringKey $luisAuthoringKey ` + --subscriptionKey $luisAuthoringKey ` + --region $region ` + --in "$(Join-Path $outFolder $outFile)" ` + --wait) 2>> $log | ConvertFrom-Json + + if (-not $luisApp) { + Write-Host "! Could not deploy LUIS model. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Log: $($log)" -ForegroundColor DarkRed + Return $null + } + else { + # train and publish luis app + $(luis train version --appId $luisApp.id --region $region --authoringKey $luisAuthoringKey --versionId $luisApp.activeVersion --wait + & luis publish version --appId $luisApp.id --region $region --authoringKey $luisAuthoringKey --versionId $luisApp.activeVersion --wait) 2>> $log | Out-Null + + Return $luisApp + } +} + +function UpdateLUIS ($lu_file, $appId, $version, $region, $authoringKey, $subscriptionKey, $log) +{ + $id = $lu_file.BaseName + $outFile = "$($id).luis" + $outFolder = $lu_file.DirectoryName + + $luisApp = luis get application --appId $appId --region $region --authoringKey $authoringKey | ConvertFrom-Json + + # Parse LU file + Write-Host "> Parsing $($id) LU file ..." + ludown parse toluis ` + --in $lu_file ` + --luis_culture $luisApp.culture ` + --out_folder $outFolder ` + --out $outFile + + Write-Host "> Getting current versions ..." + # Get list of current versions + $versions = luis list versions ` + --appId $appId ` + --region $region ` + --authoringKey $authoringKey | ConvertFrom-Json + + # If the current version exists + if ($versions | Where { $_.version -eq $version }) + { + # delete any old backups + if ($versions | Where { $_.version -eq "backup" }) + { + Write-Host "> Deleting old backup version ..." + luis delete version ` + --appId $appId ` + --versionId "backup" ` + --region $region ` + --authoringKey $authoringKey ` + --force --wait | Out-Null + } + + # rename the active version to backup + Write-Host "> Saving current version as backup ..." + luis rename version ` + --appId $appId ` + --versionId $version ` + --region $region ` + --newVersionId backup ` + --authoringKey $authoringKey ` + --subscriptionKey $subscriptionKey ` + --wait | Out-Null + } + + # import the new 0.1 version from the .luis file + Write-Host "> Importing new version ..." + luis import version ` + --appId $appId ` + --versionId $version ` + --region $region ` + --authoringKey $authoringKey ` + --subscriptionKey $subscriptionKey ` + --in "$(Join-Path $outFolder $outFile)" ` + --wait | ConvertFrom-Json + + # train and publish luis app + $(luis train version --appId $appId --region $region --authoringKey $authoringKey --versionId $version --wait + & luis publish version --appId $appId --region $region --authoringKey $authoringKey --versionId $version --wait) 2>> $log | Out-Null +} + +function RunLuisGen($lu_file, $outName, $outFolder) { + $id = $lu_file.BaseName + $luisFolder = $lu_file.DirectoryName + $luisFile = Join-Path $luisFolder "$($id).luis" + + luisgen $luisFile -cs "$($outName)Luis" -o $outFolder +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/publish.ps1 b/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/publish.ps1 new file mode 100644 index 0000000000..9483ef1ce5 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/publish.ps1 @@ -0,0 +1,66 @@ +#Requires -Version 6 + +Param( + [string] $name, + [string] $resourceGroup, + [string] $projFolder = $(Get-Location), + [string] $logFile = $(Join-Path $PSScriptRoot .. "publish_log.txt") +) + +# Get mandatory parameters +if (-not $name) { + $name = Read-Host "? Bot Web App Name" +} + +if (-not $resourceGroup) { + $resourceGroup = Read-Host "? Bot Resource Group" +} + +# Reset log file +if (Test-Path $logFile) { + Clear-Content $logFile -Force | Out-Null +} +else { + New-Item -Path $logFile | Out-Null +} + +# Check for existing deployment files +if (-not (Test-Path (Join-Path $projFolder '.deployment'))) { + + # Get path to csproj file + $projFile = Get-ChildItem $projFolder ` + | Where-Object {$_.extension -eq ".csproj" } ` + | Select-Object -First 1 + + # Add needed deployment files for az + az bot prepare-deploy --lang Csharp --code-dir $projFolder --proj-file-path $projFile.name --output json | Out-Null +} + +# Delete src zip, if it exists +$zipPath = $(Join-Path $projFolder 'code.zip') +if (Test-Path $zipPath) { + Remove-Item $zipPath -Force | Out-Null +} + +# Perform dotnet publish step ahead of zipping up +$publishFolder = $(Join-Path $projFolder 'bin\Release\netcoreapp2.2') +dotnet publish -c release -o $publishFolder -v q > $logFile + +if($?) +{ + # Compress source code + Get-ChildItem -Path "$($publishFolder)" | Compress-Archive -DestinationPath "$($zipPath)" -Force | Out-Null + + # Publish zip to Azure + Write-Host "> Publishing to Azure ..." -ForegroundColor Green + (az webapp deployment source config-zip ` + --resource-group $resourceGroup ` + --name $name ` + --src $zipPath ` + --output json) 2>> $logFile | Out-Null +} +else +{ + Write-Host "! Could not deploy automatically to Azure. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/qna_functions.ps1 b/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/qna_functions.ps1 new file mode 100644 index 0000000000..cac35fae67 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/qna_functions.ps1 @@ -0,0 +1,87 @@ +function DeployKB ($name, $lu_file, $qnaSubscriptionKey, $log) +{ + $id = $lu_file.BaseName + $outFile = "$($id).qna" + $outFolder = $lu_file.DirectoryName + + # Parse LU file + Write-Host "> Parsing $($id) LU file ..." + ludown parse toqna ` + --in $lu_file ` + --out_folder $outFolder ` + --out $outFile + + # Create QnA Maker kb + Write-Host "> Deploying $($id) QnA kb ..." + + # These values pretty much guarantee success. We can decrease them if the QnA backend gets faster + $initialDelaySeconds = 30; + $retryAttemptsRemaining = 3; + $retryDelaySeconds = 15; + $retryDelayIncrease = 30; + + while ($retryAttemptsRemaining -ge 0) { + $qnaKb = (qnamaker create kb ` + --name $id ` + --subscriptionKey $qnaSubscriptionKey ` + --in $(Join-Path $outFolder $outFile) ` + --force ` + --wait ` + --msbot) 2>> $log + + if (-not $qnaKb) { + $retryAttemptsRemaining = $retryAttemptsRemaining - 1 + Write-Host $retryAttemptsRemaining + Start-Sleep -s $retryDelaySeconds + $retryDelaySeconds += $retryDelayIncrease + + if ($retryAttemptsRemaining -lt 0) { + Write-Host "! Unable to create QnA KB." -ForegroundColor Cyan + } + else { + Write-Host "> Retrying ..." + Continue + } + } + else { + Break + } + } + + if (-not $qnaKb) { + Write-Host "! Could not deploy knowledgebase. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Log: $($log)" -ForegroundColor DarkRed + Return $null + } + else { + $qnaKb = $qnaKb | ConvertFrom-Json + + # Publish QnA Maker knowledgebase + $(qnamaker publish kb --kbId $qnaKb.kbId --subscriptionKey $qnaSubscriptionKey) 2>> $log | Out-Null + + Return $qnaKb + } +} + +function UpdateKB ($lu_file, $kbId, $qnaSubscriptionKey) +{ + $id = $lu_file.BaseName + $outFile = "$($id).qna" + $outFolder = $lu_file.DirectoryName + + # Parse LU file + Write-Host "> Parsing $($id) LU file ..." + ludown parse toqna ` + --in $lu_file ` + --out_folder $outFolder ` + --out $outFile + + Write-Host "> Replacing $($id) QnA kb ..." + qnamaker replace kb ` + --in $(Join-Path $outFolder $outFile) ` + --kbId $kbId ` + --subscriptionKey $qnaSubscriptionKey + + # Publish QnA Maker knowledgebase + $(qnamaker publish kb --kbId $kbId --subscriptionKey $qnaSubscriptionKey) 2>&1 | Out-Null +} diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/update_cognitive_models.ps1 b/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/update_cognitive_models.ps1 new file mode 100644 index 0000000000..d435017c30 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Scripts/update_cognitive_models.ps1 @@ -0,0 +1,159 @@ +#Requires -Version 6 + +Param( + [switch] $RemoteToLocal, + [switch] $useLuisGen = $true, + [string] $configFile = $(Join-Path (Get-Location) 'cognitivemodels.json'), + [string] $dispatchFolder = $(Join-Path $PSScriptRoot '..' 'Resources' 'Dispatch'), + [string] $luisFolder = $(Join-Path $PSScriptRoot '..' 'Resources' 'LU'), + [string] $qnaFolder = $(Join-Path $PSScriptRoot '..' 'Resources' 'QnA'), + [string] $lgOutFolder = $(Join-Path (Get-Location) 'Services'), + [string] $logFile = $(Join-Path $PSScriptRoot .. "update_cognitive_models_log.txt") +) + +. $PSScriptRoot\luis_functions.ps1 +. $PSScriptRoot\qna_functions.ps1 + +# Reset log file +if (Test-Path $logFile) { + Clear-Content $logFile -Force | Out-Null +} +else { + New-Item -Path $logFile | Out-Null +} + +Write-Host "> Getting config file ..." +$languageMap = @{ } +$config = Get-Content -Raw -Path $configFile | ConvertFrom-Json +$config.cognitiveModels.PSObject.Properties | Foreach-Object { $languageMap[$_.Name] = $_.Value } + +foreach ($langCode in $languageMap.Keys) { + $models = $languageMap[$langCode] + $dispatch = $models.dispatchModel + + if ($RemoteToLocal) { + # Update local LU files based on hosted models + foreach ($luisApp in $models.languageModels) { + $culture = (luis get application ` + --appId $luisApp.appId ` + --authoringKey $luisApp.authoringKey ` + --subscriptionKey $luisApp.subscriptionKey ` + --region $luisApp.authoringRegion | ConvertFrom-Json).culture + + Write-Host "> Updating local $($luisApp.id).lu file ..." + luis export version ` + --appId $luisApp.appid ` + --versionId $luisApp.version ` + --region $luisApp.authoringRegion ` + --authoringKey $luisApp.authoringKey | ludown refresh ` + --stdin ` + -n "$($luisApp.id).lu" ` + -o $(Join-Path $luisFolder $langCode) + + # Parse LU file + $id = $luisApp.id + $outFile = "$($id).luis" + $outFolder = $(Join-Path $luisFolder $langCode) + $appName = "$($name)$($langCode)_$($id)" + + Write-Host "> Parsing $($luisApp.id) LU file ..." + ludown parse toluis ` + --in $(Join-Path $outFolder "$($luisApp.id).lu") ` + --luis_culture $culture ` + --out_folder $(Join-Path $luisFolder $langCode) ` + --out "$($luisApp.id).luis" + + if ($useLuisGen) { + Write-Host "> Running LuisGen for $($luisApp.id) app ..." + $luPath = $(Join-Path $luisFolder $langCode "$($luisApp.id).lu") + RunLuisGen -lu_file $(Get-Item $luPath) -outName "$($luisApp.id)" -outFolder $lgOutFolder + } + + # Add the LUIS application to the dispatch model. + # If the LUIS application id already exists within the model no action will be taken + if ($dispatch) { + Write-Host "> Adding $($luisApp.id) app to dispatch model ... " + (dispatch add ` + --type "luis" ` + --name $luisApp.name ` + --id $luisApp.appid ` + --region $luisApp.authoringRegion ` + --intentName "l_$($luisApp.id)" ` + --dispatch $(Join-Path $dispatchFolder $langCode "$($dispatch.name).dispatch") ` + --dataFolder $(Join-Path $dispatchFolder $langCode)) 2>> $logFile | Out-Null + } + } + + # Update local LU files based on hosted QnA KBs + foreach ($kb in $models.knowledgebases) { + Write-Host "> Updating local $($kb.id).lu file ..." + qnamaker export kb ` + --environment Prod ` + --kbId $kb.kbId ` + --subscriptionKey $kb.subscriptionKey | ludown refresh ` + --stdin ` + -n "$($kb.id).lu" ` + -o $(Join-Path $qnaFolder $langCode) + + # Add the knowledge base to the dispatch model. + # If the knowledge base id already exists within the model no action will be taken + if ($dispatch) { + Write-Host "> Adding $($kb.id) kb to dispatch model ..." + (dispatch add ` + --type "qna" ` + --name $kb.name ` + --id $kb.kbId ` + --key $kb.subscriptionKey ` + --intentName "q_$($kb.id)" ` + --dispatch $(Join-Path $dispatchFolder $langCode "$($dispatch.name).dispatch") ` + --dataFolder $(Join-Path $dispatchFolder $langCode)) 2>> $logFile | Out-Null + } + } + } + else { + # Update each luis model based on local LU files + foreach ($luisApp in $models.languageModels) { + Write-Host "> Updating hosted $($luisApp.id) app..." + $lu = Get-Item -Path $(Join-Path $luisFolder $langCode "$($luisApp.id).lu") + UpdateLUIS ` + -lu_file $lu ` + -appId $luisApp.appid ` + -version $luisApp.version ` + -region $luisApp.authoringRegion ` + -authoringKey $luisApp.authoringKey ` + -subscriptionKey $app.subscriptionKey + + if ($useLuisGen) { + Write-Host "> Running LuisGen for $($luisApp.id) app ..." + $luPath = $(Join-Path $luisFolder $langCode "$($luisApp.id).lu") + RunLuisGen -lu_file $(Get-Item $luPath) -outName "$($luisApp.id)" -outFolder $lgOutFolder + } + } + + # Update each knowledgebase based on local LU files + foreach ($kb in $models.knowledgebases) { + Write-Host "> Updating hosted $($kb.id) kb..." + $lu = Get-Item -Path $(Join-Path $qnaFolder $langCode "$($kb.id).lu") + UpdateKB ` + -lu_file $lu ` + -kbId $kb.kbId ` + -qnaSubscriptionKey $kb.subscriptionKey + } + } + + if ($dispatch) { + # Update dispatch model + Write-Host "> Updating dispatch model ..." + dispatch refresh ` + --dispatch $(Join-Path $dispatchFolder $langCode "$($dispatch.name).dispatch") ` + --dataFolder $(Join-Path $dispatchFolder $langCode) 2>> $logFile | Out-Null + + if ($useLuisGen) { + # Update dispatch.cs file + Write-Host "> Running LuisGen for Dispatch app..." + luisgen $(Join-Path $dispatchFolder $langCode "$($dispatch.name).json") -cs "DispatchLuis" -o $lgOutFolder 2>> $logFile | Out-Null + } + } +} + +Write-Host "> Done." \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs new file mode 100644 index 0000000000..0208d76286 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ITSMSkill.Models; +using ITSMSkill.Responses.CreateTicket; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Services; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Dialogs.Choices; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Connector; + +namespace ITSMSkill.Dialogs +{ + public class CreateTicketDialog : SkillDialogBase + { + public CreateTicketDialog( + BotSettings settings, + BotServices services, + ResponseManager responseManager, + ConversationState conversationState, + IServiceManager serviceManager, + IBotTelemetryClient telemetryClient) + : base(nameof(CreateTicketDialog), settings, services, responseManager, conversationState, serviceManager, telemetryClient) + { + var createTicket = new WaterfallStep[] + { + CheckDescription, + InputDescription, + SetDescription, + GetAuthToken, + AfterGetAuthToken, + DisplayExisting, + IfExistingSolve, + CheckUrgency, + InputUrgency, + SetUrgency, + GetAuthToken, + AfterGetAuthToken, + CreateTicket + }; + AddDialog(new WaterfallDialog(Actions.CreateTicket, createTicket) { TelemetryClient = telemetryClient }); + + InitialDialogId = Actions.CreateTicket; + } + + public async Task CheckDescription(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (string.IsNullOrEmpty(state.TicketDescription)) + { + return await sc.NextAsync(false); + } + else + { + var replacements = new StringDictionary + { + { "Description", state.TicketDescription } + }; + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.ConfirmDescription, replacements) + }; + + return await sc.PromptAsync(nameof(ConfirmPrompt), options); + } + } + + public async Task InputDescription(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (!(bool)sc.Result || string.IsNullOrEmpty(state.TicketDescription)) + { + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.InputDescription) + }; + + return await sc.PromptAsync(nameof(TextPrompt), options); + } + else + { + return await sc.NextAsync(state.TicketDescription); + } + } + + public async Task SetDescription(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.TicketDescription = (string)sc.Result; + return await sc.NextAsync(); + } + + public async Task DisplayExisting(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (state.Token == null) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); + return await sc.EndDialogAsync(); + } + + var management = ServiceManager.CreateManagement(Settings, state.Token); + var result = await management.SearchKnowledge(state.TicketDescription); + + if (!result.Success) + { + var errorReplacements = new StringDictionary + { + { "Error", result.ErrorMessage } + }; + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); + return await sc.EndDialogAsync(); + } + + if (result.Knowledges == null || result.Knowledges.Length == 0) + { + return await sc.NextAsync(false); + } + else + { + var cards = new List(); + foreach (var knowledge in result.Knowledges) + { + cards.Add(new Card() + { + Name = GetDivergedCardName(sc.Context, "Knowledge"), + Data = ConvertKnowledge(knowledge) + }); + } + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetCardResponse(SharedResponses.IfExistingSolve, cards) + }; + + // Workaround. In teams, HeroCard will be used for prompt and adaptive card could not be shown. So send them separatly + if (Channel.GetChannelId(sc.Context) == Channels.Msteams) + { + await sc.Context.SendActivityAsync(options.Prompt); + options.Prompt = null; + } + + return await sc.PromptAsync(nameof(ConfirmPrompt), options); + } + } + + public async Task IfExistingSolve(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + if ((bool)sc.Result) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ExistingSolve)); + return await sc.EndDialogAsync(); + } + else + { + return await sc.NextAsync(); + } + } + + public async Task CheckUrgency(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (state.UrgencyLevel == UrgencyLevel.None) + { + return await sc.NextAsync(false); + } + else + { + var replacements = new StringDictionary + { + { "Urgency", state.UrgencyLevel.ToString() } + }; + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.ConfirmUrgency, replacements) + }; + + return await sc.PromptAsync(nameof(ConfirmPrompt), options); + } + } + + public async Task InputUrgency(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (!(bool)sc.Result || state.UrgencyLevel == UrgencyLevel.None) + { + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.InputUrgency), + Choices = new List() + { + new Choice() + { + Value = UrgencyLevel.Low.ToString() + }, + new Choice() + { + Value = UrgencyLevel.Medium.ToString() + }, + new Choice() + { + Value = UrgencyLevel.High.ToString() + } + } + }; + + return await sc.PromptAsync(nameof(ChoicePrompt), options); + } + else + { + return await sc.NextAsync(new FoundChoice() + { + Value = state.UrgencyLevel.ToString() + }); + } + } + + public async Task SetUrgency(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + Enum.TryParse(((FoundChoice)sc.Result).Value, out UrgencyLevel urgency); + state.UrgencyLevel = urgency; + return await sc.NextAsync(); + } + + public async Task CreateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (state.Token == null) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); + return await sc.EndDialogAsync(); + } + + var management = ServiceManager.CreateManagement(Settings, state.Token); + var result = await management.CreateTicket(state.TicketDescription, state.UrgencyLevel); + + if (!result.Success) + { + var errorReplacements = new StringDictionary + { + { "Error", result.ErrorMessage } + }; + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); + return await sc.EndDialogAsync(); + } + + var card = new Card() + { + Name = GetDivergedCardName(sc.Context, "Ticket"), + Data = ConvertTicket(result.Ticket) + }; + + await sc.Context.SendActivityAsync(ResponseManager.GetCardResponse(CreateTicketResponses.TicketCreated, card, null)); + return await sc.NextAsync(); + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs new file mode 100644 index 0000000000..d0642d0d4a --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using ITSMSkill.Models; +using ITSMSkill.Responses.Main; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Services; +using Luis; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Dialogs.Choices; +using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Builder.Skills.Models; +using Microsoft.Bot.Builder.Solutions; +using Microsoft.Bot.Builder.Solutions.Dialogs; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Connector; +using Microsoft.Bot.Schema; + +namespace ITSMSkill.Dialogs +{ + public class MainDialog : RouterDialog + { + private BotSettings _settings; + private BotServices _services; + private ResponseManager _responseManager; + private IStatePropertyAccessor _stateAccessor; + private IStatePropertyAccessor _contextAccessor; + + public MainDialog( + BotSettings settings, + BotServices services, + ResponseManager responseManager, + UserState userState, + ConversationState conversationState, + CreateTicketDialog createTicketDialog, + IBotTelemetryClient telemetryClient) + : base(nameof(MainDialog), telemetryClient) + { + _settings = settings; + _services = services; + _responseManager = responseManager; + TelemetryClient = telemetryClient; + + // Initialize state accessor + _stateAccessor = conversationState.CreateProperty(nameof(SkillState)); + _contextAccessor = userState.CreateProperty(nameof(SkillContext)); + + // Register dialogs + AddDialog(createTicketDialog); + } + + protected override async Task OnStartAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) + { + var locale = CultureInfo.CurrentUICulture; + await dc.Context.SendActivityAsync(_responseManager.GetResponse(MainResponses.WelcomeMessage)); + } + + protected override async Task RouteAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) + { + // get current activity locale + var locale = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; + var localeConfig = _services.CognitiveModelSets[locale]; + + // Populate state from SemanticAction as required + await PopulateStateFromSemanticAction(dc.Context); + + // Get skill LUIS model from configuration + localeConfig.LuisServices.TryGetValue("ITSM", out var luisService); + + if (luisService == null) + { + throw new Exception("The specified LUIS Model could not be found in your Bot Services configuration."); + } + else + { + var turnResult = EndOfTurn; + var result = await luisService.RecognizeAsync(dc.Context, CancellationToken.None); + var intent = result?.TopIntent().intent; + + if (intent != ITSMLuis.Intent.None) + { + var state = await _stateAccessor.GetAsync(dc.Context, () => new SkillState()); + state.DigestLuisResult(result); + } + + switch (intent) + { + case ITSMLuis.Intent.TicketCreate: + { + turnResult = await dc.BeginDialogAsync(nameof(CreateTicketDialog)); + break; + } + + case ITSMLuis.Intent.None: + { + // No intent was identified, send confused message + await dc.Context.SendActivityAsync(_responseManager.GetResponse(SharedResponses.DidntUnderstandMessage)); + turnResult = new DialogTurnResult(DialogTurnStatus.Complete); + break; + } + + default: + { + // intent was identified but not yet implemented + await dc.Context.SendActivityAsync(_responseManager.GetResponse(MainResponses.FeatureNotAvailable)); + turnResult = new DialogTurnResult(DialogTurnStatus.Complete); + break; + } + } + + if (turnResult != EndOfTurn) + { + await CompleteAsync(dc); + } + } + } + + protected override async Task CompleteAsync(DialogContext dc, DialogTurnResult result = null, CancellationToken cancellationToken = default(CancellationToken)) + { + // workaround. if connect skill directly to teams, the following response does not work. + if (dc.Context.Adapter is IRemoteUserTokenProvider remoteInvocationAdapter || Channel.GetChannelId(dc.Context) != Channels.Msteams) + { + var response = dc.Context.Activity.CreateReply(); + response.Type = ActivityTypes.EndOfConversation; + await dc.Context.SendActivityAsync(response); + } + + await dc.EndDialogAsync(result); + } + + protected override async Task OnEventAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) + { + switch (dc.Context.Activity.Name) + { + case TokenEvents.TokenResponseEventName: + { + // 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); + } + + break; + } + } + } + + protected override async Task OnInterruptDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) + { + var result = InterruptionAction.NoAction; + + if (dc.Context.Activity.Type == ActivityTypes.Message) + { + // get current activity locale + var locale = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; + var localeConfig = _services.CognitiveModelSets[locale]; + + // check general luis intent + localeConfig.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(); + + if (topIntent.score > 0.5) + { + switch (topIntent.intent) + { + case GeneralLuis.Intent.Cancel: + { + result = await OnCancel(dc); + break; + } + + case GeneralLuis.Intent.Help: + { + result = await OnHelp(dc); + break; + } + + case GeneralLuis.Intent.Logout: + { + result = await OnLogout(dc); + break; + } + } + } + } + } + + return result; + } + + private async Task OnCancel(DialogContext dc) + { + await dc.Context.SendActivityAsync(_responseManager.GetResponse(MainResponses.CancelMessage)); + await CompleteAsync(dc); + await dc.CancelAllDialogsAsync(); + return InterruptionAction.StartedDialog; + } + + private async Task OnHelp(DialogContext dc) + { + await dc.Context.SendActivityAsync(_responseManager.GetResponse(MainResponses.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 + var tokens = await adapter.GetTokenStatusAsync(dc.Context, dc.Context.Activity.From.Id); + foreach (var token in tokens) + { + await adapter.SignOutUserAsync(dc.Context, token.ConnectionName); + } + + await dc.Context.SendActivityAsync(_responseManager.GetResponse(MainResponses.LogOut)); + + return InterruptionAction.StartedDialog; + } + + private async Task PopulateStateFromSemanticAction(ITurnContext context) + { + // Example of populating local state with data passed through semanticAction out of Activity + var activity = context.Activity; + var semanticAction = activity.SemanticAction; + + // if (semanticAction != null && semanticAction.Entities.ContainsKey("location")) + // { + // var location = semanticAction.Entities["location"]; + // var locationObj = location.Properties["location"].ToString(); + // var state = await _stateAccessor.GetAsync(context, () => new SkillState()); + // state.CurrentCoordinates = locationObj; + // } + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs new file mode 100644 index 0000000000..4b5c83f89a --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ITSMSkill.Models; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Services; +using Luis; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Dialogs.Choices; +using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Builder.Solutions.Authentication; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Builder.Solutions.Util; +using Microsoft.Bot.Connector; +using Microsoft.Bot.Schema; +using Microsoft.Graph; + +namespace ITSMSkill.Dialogs +{ + public class SkillDialogBase : ComponentDialog + { + public SkillDialogBase( + string dialogId, + BotSettings settings, + BotServices services, + ResponseManager responseManager, + ConversationState conversationState, + IServiceManager serviceManager, + IBotTelemetryClient telemetryClient) + : base(dialogId) + { + Settings = settings; + Services = services; + ResponseManager = responseManager; + StateAccessor = conversationState.CreateProperty(nameof(SkillState)); + ServiceManager = serviceManager; + TelemetryClient = telemetryClient; + + // NOTE: Uncomment the following if your skill requires authentication + if (!settings.OAuthConnections.Any()) + { + throw new Exception("You must configure an authentication connection before using this component."); + } + + AddDialog(new MultiProviderAuthDialog(settings.OAuthConnections)); + + AddDialog(new TextPrompt(nameof(TextPrompt))); + AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt))); + AddDialog(new ChoicePrompt(nameof(ChoicePrompt))); + } + + protected BotSettings Settings { get; set; } + + protected BotServices Services { get; set; } + + protected IStatePropertyAccessor StateAccessor { get; set; } + + protected ResponseManager ResponseManager { get; set; } + + protected IServiceManager ServiceManager { get; set; } + + protected override async Task OnBeginDialogAsync(DialogContext dc, object options, CancellationToken cancellationToken = default(CancellationToken)) + { + return await base.OnBeginDialogAsync(dc, options, cancellationToken); + } + + protected override async Task OnContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) + { + return await base.OnContinueDialogAsync(dc, cancellationToken); + } + + protected async Task GetAuthToken(WaterfallStepContext sc, CancellationToken cancellationToken) + { + try + { + return await sc.PromptAsync(nameof(MultiProviderAuthDialog), new PromptOptions()); + } + catch (SkillException ex) + { + await HandleDialogExceptions(sc, ex); + return new DialogTurnResult(DialogTurnStatus.Cancelled, CommonUtil.DialogTurnResultCancelAllDialogs); + } + catch (Exception ex) + { + await HandleDialogExceptions(sc, ex); + return new DialogTurnResult(DialogTurnStatus.Cancelled, CommonUtil.DialogTurnResultCancelAllDialogs); + } + } + + protected async Task AfterGetAuthToken(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + try + { + // When the user authenticates interactively we pass on the tokens/Response event which surfaces as a JObject + // When the token is cached we get a TokenResponse object. + if (sc.Result is ProviderTokenResponse providerTokenResponse) + { + var state = await StateAccessor.GetAsync(sc.Context); + state.Token = providerTokenResponse.TokenResponse; + } + + return await sc.NextAsync(); + } + catch (SkillException ex) + { + await HandleDialogExceptions(sc, ex); + return new DialogTurnResult(DialogTurnStatus.Cancelled, CommonUtil.DialogTurnResultCancelAllDialogs); + } + catch (Exception ex) + { + await HandleDialogExceptions(sc, ex); + return new DialogTurnResult(DialogTurnStatus.Cancelled, CommonUtil.DialogTurnResultCancelAllDialogs); + } + } + + // Validators + protected Task TokenResponseValidator(PromptValidatorContext pc, CancellationToken cancellationToken) + { + var activity = pc.Recognized.Value; + if (activity != null && activity.Type == ActivityTypes.Event) + { + return Task.FromResult(true); + } + else + { + return Task.FromResult(false); + } + } + + protected Task AuthPromptValidator(PromptValidatorContext promptContext, CancellationToken cancellationToken) + { + var token = promptContext.Recognized.Value; + if (token != null) + { + return Task.FromResult(true); + } + else + { + return Task.FromResult(false); + } + } + + // Helpers + // This method is called by any waterfall step that throws an exception to ensure consistency + protected async Task HandleDialogExceptions(WaterfallStepContext sc, Exception ex) + { + // send trace back to emulator + var trace = new Activity(type: ActivityTypes.Trace, text: $"DialogException: {ex.Message}, StackTrace: {ex.StackTrace}"); + await sc.Context.SendActivityAsync(trace); + + // log exception + TelemetryClient.TrackException(ex, new Dictionary { { nameof(sc.ActiveDialog), sc.ActiveDialog?.Id } }); + + // send error message to bot user + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ErrorMessage)); + + // clear state + var state = await StateAccessor.GetAsync(sc.Context); + state.Clear(); + } + + protected TicketCard ConvertTicket(Ticket ticket) + { + var card = new TicketCard() + { + Description = ticket.Description, + UrgencyLevel = $"{SharedStrings.Urgency}{ConvertUrgencyLevel(ticket.Urgency)}", + State = $"{SharedStrings.TicketState}{ConvertTicketState(ticket.State)}", + OpenedTime = $"{SharedStrings.OpenedAt}{ticket.OpenedTime.ToString()}", + Id = ticket.Id, + ResolvedReason = ticket.ResolvedReason, + Speak = ticket.Description + }; + return card; + } + + protected KnowledgeCard ConvertKnowledge(Knowledge knowledge) + { + var card = new KnowledgeCard() + { + Id = knowledge.Id, + Title = knowledge.Title, + UpdatedTime = $"{SharedStrings.UpdatedAt}{knowledge.UpdatedTime.ToString()}", + Content = knowledge.Content, + Speak = knowledge.Title + }; + return card; + } + + protected string ConvertUrgencyLevel(UrgencyLevel urgency) + { + switch (urgency) + { + case UrgencyLevel.Low: return SharedStrings.UrgencyLow; + case UrgencyLevel.Medium: return SharedStrings.UrgencyMedium; + case UrgencyLevel.High: return SharedStrings.UrgencyHigh; + default: return string.Empty; + } + } + + protected string ConvertTicketState(TicketState state) + { + switch (state) + { + case TicketState.New: return SharedStrings.TicketStateNew; + case TicketState.InProgress: return SharedStrings.TicketStateInProgress; + case TicketState.OnHold: return SharedStrings.TicketStateOnHold; + case TicketState.Resolved: return SharedStrings.TicketStateResolved; + case TicketState.Closed: return SharedStrings.TicketStateClosed; + case TicketState.Canceled: return SharedStrings.TicketStateCanceled; + default: return string.Empty; + } + } + + protected string GetDivergedCardName(ITurnContext turnContext, string card) + { + if (Channel.GetChannelId(turnContext) == Channels.Msteams) + { + return card + ".1.0"; + } + else + { + return card; + } + } + + protected class Actions + { + public const string CreateTicket = "CreateTicket"; + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj new file mode 100644 index 0000000000..71703eb6cb --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj @@ -0,0 +1,124 @@ + + + + netcoreapp2.2 + NU1701 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + + TextTemplatingFileGenerator + CreateTicketResponses.cs + + + TextTemplatingFileGenerator + MainResponses.cs + + + TextTemplatingFileGenerator + MainResponses.cs + + + TextTemplatingFileGenerator + SharedResponses.cs + + + + + + + + + + + + + + True + True + CreateTicketResponses.tt + + + True + True + MainResponses.tt + + + True + True + MainResponses.tt + + + True + True + SharedResponses.tt + + + True + True + SharedStrings.resx + + + + + + PublicResXFileCodeGenerator + SharedStrings.Designer.cs + + + + diff --git a/skills/src/csharp/experimental/itsmskill/Models/CreateTicketResult.cs b/skills/src/csharp/experimental/itsmskill/Models/CreateTicketResult.cs new file mode 100644 index 0000000000..39298903de --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/CreateTicketResult.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models +{ + public class CreateTicketResult : ResultBase + { + public Ticket Ticket { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/Knowledge.cs b/skills/src/csharp/experimental/itsmskill/Models/Knowledge.cs new file mode 100644 index 0000000000..822edf43f5 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/Knowledge.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models +{ + public class Knowledge + { + public string Id { get; set; } + + public string Title { get; set; } + + public DateTime UpdatedTime { get; set; } + + public string Content { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/KnowledgeCard.cs b/skills/src/csharp/experimental/itsmskill/Models/KnowledgeCard.cs new file mode 100644 index 0000000000..26045e46af --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/KnowledgeCard.cs @@ -0,0 +1,17 @@ +using Microsoft.Bot.Builder.Solutions.Responses; + +namespace ITSMSkill.Models +{ + public class KnowledgeCard : ICardData + { + public string Id { get; set; } + + public string Title { get; set; } + + public string UpdatedTime { get; set; } + + public string Content { get; set; } + + public string Speak { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/ResultBase.cs b/skills/src/csharp/experimental/itsmskill/Models/ResultBase.cs new file mode 100644 index 0000000000..023925e326 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ResultBase.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models +{ + public class ResultBase + { + public bool Success { get; set; } + + public string ErrorMessage { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/SearchKnowledgeResult.cs b/skills/src/csharp/experimental/itsmskill/Models/SearchKnowledgeResult.cs new file mode 100644 index 0000000000..92db908209 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/SearchKnowledgeResult.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models +{ + public class SearchKnowledgeResult : ResultBase + { + public Knowledge[] Knowledges { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/SearchTicketResult.cs b/skills/src/csharp/experimental/itsmskill/Models/SearchTicketResult.cs new file mode 100644 index 0000000000..e635b9a22a --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/SearchTicketResult.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models +{ + public class SearchTicketResult : ResultBase + { + public Ticket[] Tickets { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs new file mode 100644 index 0000000000..4eedc3b963 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs @@ -0,0 +1,9 @@ +namespace ITSMSkill.Models.ServiceNow +{ + public class CreateTicketRequest + { + public string short_description { get; set; } + + public string urgency { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketResponse.cs new file mode 100644 index 0000000000..5f94a3eb3d --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketResponse.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models.ServiceNow +{ + public class CreateTicketResponse + { + public TicketResponse result { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/KnowledgeResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/KnowledgeResponse.cs new file mode 100644 index 0000000000..1f095d0394 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/KnowledgeResponse.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models.ServiceNow +{ + public class KnowledgeResponse + { + public string short_description { get; set; } + + public string sys_updated_on { get; set; } + + public string sys_id { get; set; } + + public string text { get; set; } + + public string wiki { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchKnowledgeResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchKnowledgeResponse.cs new file mode 100644 index 0000000000..16bf63b363 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchKnowledgeResponse.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models.ServiceNow +{ + public class SearchKnowledgeResponse + { + public List result { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchTicketResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchTicketResponse.cs new file mode 100644 index 0000000000..10e1cf4c7c --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchTicketResponse.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models.ServiceNow +{ + public class SearchTicketResponse + { + public List result { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/TicketResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/TicketResponse.cs new file mode 100644 index 0000000000..2a35f7135f --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/TicketResponse.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models.ServiceNow +{ + public class TicketResponse + { + public UserInfo opened_by { get; set; } + + public string state { get; set; } + + public string opened_at { get; set; } + + public string short_description { get; set; } + + public string close_code { get; set; } + + public string close_notes { get; set; } + + public string sys_id { get; set; } + + public string urgency { get; set; } + + public class UserInfo + { + public string value { get; set; } + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs new file mode 100644 index 0000000000..ae6a387f57 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using ITSMSkill.Services; +using Luis; +using Microsoft.Bot.Schema; + +namespace ITSMSkill.Models +{ + public class SkillState + { + public SkillState() + { + Clear(); + } + + public TokenResponse Token { get; set; } + + public string TicketDescription { get; set; } + + public UrgencyLevel UrgencyLevel { get; set; } + + public void DigestLuisResult(ITSMLuis luis) + { + Clear(); + if (luis.Entities.TicketDescription != null) + { + TicketDescription = string.Join(' ', luis.Entities.TicketDescription); + } + } + + public void Clear() + { + Token = null; + TicketDescription = null; + UrgencyLevel = UrgencyLevel.None; + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/Ticket.cs b/skills/src/csharp/experimental/itsmskill/Models/Ticket.cs new file mode 100644 index 0000000000..f9d22498b5 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/Ticket.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models +{ + public class Ticket + { + public string Id { get; set; } + + public string Description { get; set; } + + public UrgencyLevel Urgency { get; set; } + + public TicketState State { get; set; } + + public DateTime OpenedTime { get; set; } + + public string ResolvedReason { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/TicketCard.cs b/skills/src/csharp/experimental/itsmskill/Models/TicketCard.cs new file mode 100644 index 0000000000..dd836c553b --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/TicketCard.cs @@ -0,0 +1,23 @@ +using Microsoft.Bot.Builder.Solutions.Responses; + +namespace ITSMSkill.Models +{ + public class TicketCard : ICardData + { + public string Description { get; set; } + + public string UrgencyColor { get; set; } + + public string UrgencyLevel { get; set; } + + public string State { get; set; } + + public string OpenedTime { get; set; } + + public string Id { get; set; } + + public string ResolvedReason { get; set; } + + public string Speak { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/TicketState.cs b/skills/src/csharp/experimental/itsmskill/Models/TicketState.cs new file mode 100644 index 0000000000..09611abc7a --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/TicketState.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models +{ + // TODO same as ServiceNow's ticket state + public enum TicketState + { + None, + New, + InProgress, + OnHold, + Resolved, + Closed, + Canceled + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/UrgencyLevel.cs b/skills/src/csharp/experimental/itsmskill/Models/UrgencyLevel.cs new file mode 100644 index 0000000000..a0bce98d30 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/UrgencyLevel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models +{ + // TODO same as ServiceNow's Urgency. However it is mapped to Priority internally + public enum UrgencyLevel + { + None, + Low, + Medium, + High + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Program.cs b/skills/src/csharp/experimental/itsmskill/Program.cs new file mode 100644 index 0000000000..893887785c --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Program.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace ITSMSkill +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() // Note: Application Insights is added in Startup. Disabling is also handled there. + .Build(); + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Properties/launchSettings.json b/skills/src/csharp/experimental/itsmskill/Properties/launchSettings.json new file mode 100644 index 0000000000..95fd5dd7b8 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:3980/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "ITSMSkill": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:1205/" + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.cs new file mode 100644 index 0000000000..cc3c83c0b1 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.cs @@ -0,0 +1,17 @@ +// https://docs.microsoft.com/en-us/visualstudio/modeling/t4-include-directive?view=vs-2017 +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder.Solutions.Responses; + +namespace ITSMSkill.Responses.CreateTicket +{ + /// + /// Contains bot responses. + /// + public class CreateTicketResponses : IResponseIdCollection + { + // Generated accessors + public const string TicketCreated = "TicketCreated"; + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.json new file mode 100644 index 0000000000..b283d76289 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.json @@ -0,0 +1,11 @@ +{ + "TicketCreated": { + "replies": [ + { + "text": "Your ticket has been created. Let me know when you need my help.", + "speak": "Your ticket has been created. Let me know when you need my help." + } + ], + "inputHint": "acceptingInput" + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.tt b/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.tt new file mode 100644 index 0000000000..f204f0981b --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.tt @@ -0,0 +1,3 @@ +<#@ template debug="false" hostspecific="true" language="C#" #> +<#@ output extension=".cs" #> +<#@ include file="..\Shared\ResponseIdCollection.t4"#> \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.cs new file mode 100644 index 0000000000..5602be5a8d --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.cs @@ -0,0 +1,23 @@ +// https://docs.microsoft.com/en-us/visualstudio/modeling/t4-include-directive?view=vs-2017 +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder.Solutions.Responses; + +namespace ITSMSkill.Responses.Main +{ + /// + /// Contains bot responses. + /// + public class MainResponses : IResponseIdCollection + { + // Generated accessors + public const string WelcomeMessage = "WelcomeMessage"; + public const string HelpMessage = "HelpMessage"; + public const string GreetingMessage = "GreetingMessage"; + public const string GoodbyeMessage = "GoodbyeMessage"; + public const string LogOut = "LogOut"; + public const string FeatureNotAvailable = "FeatureNotAvailable"; + public const string CancelMessage = "CancelMessage"; + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.json new file mode 100644 index 0000000000..9c4f2f4e7f --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.json @@ -0,0 +1,83 @@ +{ + "WelcomeMessage": { + "replies": [ + { + "text": "Welcome to IT Service Managerment skill! You can view, create or resolve tickets here.", + "speak": "Welcome to IT Service Managerment skill! You can view, create or resolve tickets here." + } + ], + "suggestedActions": [], + "inputHint": "acceptingInput" + }, + "HelpMessage": { + "replies": [ + { + "text": "Try \"open a ticket\".", + "speak": "Try \"open a ticket\"." + } + ], + "suggestedActions": [], + "inputHint": "acceptingInput" + }, + "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 this skill. Please try asking something else.", + "speak": "This feature is not yet available in this skill. Please try asking something else." + } + ], + "inputHint": "acceptingInput" + }, + "CancelMessage": { + "replies": [ + { + "text": "Ok, let's start over.", + "speak": "Ok, let's start over." + } + ], + "inputHint": "acceptingInput" + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.tt b/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.tt new file mode 100644 index 0000000000..f204f0981b --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.tt @@ -0,0 +1,3 @@ +<#@ template debug="false" hostspecific="true" language="C#" #> +<#@ output extension=".cs" #> +<#@ include file="..\Shared\ResponseIdCollection.t4"#> \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/ResponseIdCollection.t4 b/skills/src/csharp/experimental/itsmskill/Responses/Shared/ResponseIdCollection.t4 new file mode 100644 index 0000000000..d6c0d7cc3e --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/ResponseIdCollection.t4 @@ -0,0 +1,31 @@ +<#@ assembly name="Newtonsoft.Json.dll" #> +<# + 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 + ".json")); + var json = Newtonsoft.Json.JsonConvert.DeserializeObject>(myFile); + var responses = string.Empty; + var cards = string.Empty; +#> +// https://docs.microsoft.com/en-us/visualstudio/modeling/t4-include-directive?view=vs-2017 +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder.Solutions.Responses; + +namespace <#= namespaceName #> +{ + /// + /// Contains bot responses. + /// + public class <#= className #> : IResponseIdCollection + { + // Generated accessors +<# +// This code runs in the text json: +foreach (var propertyName in json) { +#> + public const string <#= propertyName.Key.Substring(0, 1).ToUpperInvariant() + propertyName.Key.Substring(1) #> = "<#= propertyName.Key #>"; +<# } #> + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs new file mode 100644 index 0000000000..eba00dbe9b --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs @@ -0,0 +1,29 @@ +// https://docs.microsoft.com/en-us/visualstudio/modeling/t4-include-directive?view=vs-2017 +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder.Solutions.Responses; + +namespace ITSMSkill.Responses.Shared +{ + /// + /// Contains bot responses. + /// + public class SharedResponses : IResponseIdCollection + { + // Generated accessors + public const string DidntUnderstandMessage = "DidntUnderstandMessage"; + public const string CancellingMessage = "CancellingMessage"; + public const string NoAuth = "NoAuth"; + public const string AuthFailed = "AuthFailed"; + public const string ActionEnded = "ActionEnded"; + public const string ErrorMessage = "ErrorMessage"; + public const string ConfirmDescription = "ConfirmDescription"; + public const string InputDescription = "InputDescription"; + public const string ConfirmUrgency = "ConfirmUrgency"; + public const string InputUrgency = "InputUrgency"; + public const string IfExistingSolve = "IfExistingSolve"; + public const string ExistingSolve = "ExistingSolve"; + public const string ServiceFailed = "ServiceFailed"; + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json new file mode 100644 index 0000000000..0e67659285 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json @@ -0,0 +1,211 @@ +{ + "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": "acceptingInput" + }, + "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": "acceptingInput" + }, + "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": "acceptingInput" + }, + "ErrorMessage": { + "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": "acceptingInput" + }, + "ConfirmDescription": { + "replies": [ + { + "text": "Is the description \"{Description}\" correct?", + "speak": "Is the description \"{Description}\" correct?" + } + ], + "inputHint": "expectingInput" + }, + "InputDescription": { + "replies": [ + { + "text": "Please input the description:", + "speak": "Please input the description:" + } + ], + "inputHint": "expectingInput" + }, + "ConfirmUrgency": { + "replies": [ + { + "text": "Is the urgency level \"{Urgency}\" correct?", + "speak": "Is the urgency level \"{Urgency}\" correct?" + } + ], + "inputHint": "expectingInput" + }, + "InputUrgency": { + "replies": [ + { + "text": "Please input the urgency level:", + "speak": "Please input the urgency level:" + } + ], + "inputHint": "expectingInput" + }, + "IfExistingSolve": { + "replies": [ + { + "text": "Does one of these solve your problem?", + "speak": "Does one of these solve your problem?" + } + ], + "inputHint": "expectingInput" + }, + "ExistingSolve": { + "replies": [ + { + "text": "Cool, let me know when you need my help.", + "speak": "Cool, let me know when you need my help." + } + ], + "inputHint": "acceptingInput" + }, + "ServiceFailed": { + "replies": [ + { + "text": "Sorry, IT service failed due to {Error}.", + "speak": "Sorry, IT service failed due to {Error}." + } + ], + "inputHint": "acceptingInput" + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.tt b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.tt new file mode 100644 index 0000000000..f204f0981b --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.tt @@ -0,0 +1,3 @@ +<#@ template debug="false" hostspecific="true" language="C#" #> +<#@ output extension=".cs" #> +<#@ include file="..\Shared\ResponseIdCollection.t4"#> \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs new file mode 100644 index 0000000000..f54d16094f --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs @@ -0,0 +1,180 @@ +//------------------------------------------------------------------------------ +// +// 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 ITSMSkill.Responses.Shared { + 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", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class SharedStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal SharedStrings() { + } + + /// + /// 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("ITSMSkill.Responses.Shared.SharedStrings", typeof(SharedStrings).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 Opened at . + /// + public static string OpenedAt { + get { + return ResourceManager.GetString("OpenedAt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to State: . + /// + public static string TicketState { + get { + return ResourceManager.GetString("TicketState", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Canceled. + /// + public static string TicketStateCanceled { + get { + return ResourceManager.GetString("TicketStateCanceled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Closed. + /// + public static string TicketStateClosed { + get { + return ResourceManager.GetString("TicketStateClosed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to In Progress. + /// + public static string TicketStateInProgress { + get { + return ResourceManager.GetString("TicketStateInProgress", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to New. + /// + public static string TicketStateNew { + get { + return ResourceManager.GetString("TicketStateNew", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to On Hold. + /// + public static string TicketStateOnHold { + get { + return ResourceManager.GetString("TicketStateOnHold", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Resolved. + /// + public static string TicketStateResolved { + get { + return ResourceManager.GetString("TicketStateResolved", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updated at . + /// + public static string UpdatedAt { + get { + return ResourceManager.GetString("UpdatedAt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Urgency: . + /// + public static string Urgency { + get { + return ResourceManager.GetString("Urgency", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to High. + /// + public static string UrgencyHigh { + get { + return ResourceManager.GetString("UrgencyHigh", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Low. + /// + public static string UrgencyLow { + get { + return ResourceManager.GetString("UrgencyLow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Medium. + /// + public static string UrgencyMedium { + get { + return ResourceManager.GetString("UrgencyMedium", resourceCulture); + } + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx new file mode 100644 index 0000000000..65ed63c9b3 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + High + + + Low + + + Medium + + + Canceled + + + Closed + + + In Progress + + + New + + + On Hold + + + Resolved + + + Opened at + + + State: + + + Updated at + + + Urgency: + + \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Services/BotServices.cs b/skills/src/csharp/experimental/itsmskill/Services/BotServices.cs new file mode 100644 index 0000000000..c702ff7407 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/BotServices.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.AI.Luis; +using Microsoft.Bot.Builder.AI.QnA; +using Microsoft.Bot.Builder.Solutions; + +namespace ITSMSkill.Services +{ + public class BotServices + { + public BotServices() + { + } + + public BotServices(BotSettings settings, IBotTelemetryClient client) + { + foreach (var pair in settings.CognitiveModels) + { + var set = new CognitiveModelSet(); + var language = pair.Key; + var config = pair.Value; + + var telemetryClient = client; + var luisOptions = new LuisPredictionOptions() + { + TelemetryClient = telemetryClient, + LogPersonalInformation = true, + }; + + if (config.DispatchModel != null) + { + var dispatchApp = new LuisApplication(config.DispatchModel.AppId, config.DispatchModel.SubscriptionKey, config.DispatchModel.GetEndpoint()); + set.DispatchService = new LuisRecognizer(dispatchApp); + } + + if (config.LanguageModels != null) + { + foreach (var model in config.LanguageModels) + { + var luisApp = new LuisApplication(model.AppId, model.SubscriptionKey, model.GetEndpoint()); + set.LuisServices.Add(model.Id, new LuisRecognizer(luisApp)); + } + } + + if (config.Knowledgebases != null) + { + foreach (var kb in config.Knowledgebases) + { + var qnaEndpoint = new QnAMakerEndpoint() + { + KnowledgeBaseId = kb.KbId, + EndpointKey = kb.EndpointKey, + Host = kb.Hostname, + }; + var qnaMaker = new QnAMaker(qnaEndpoint); + set.QnAServices.Add(kb.Id, qnaMaker); + } + } + + CognitiveModelSets.Add(language, set); + } + } + + public Dictionary CognitiveModelSets { get; set; } = new Dictionary(); + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Services/BotSettings.cs b/skills/src/csharp/experimental/itsmskill/Services/BotSettings.cs new file mode 100644 index 0000000000..58b9d6c66d --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/BotSettings.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder.Solutions; + +namespace ITSMSkill.Services +{ + public class BotSettings : BotSettingsBase + { + public string ServiceNowUrl { get; set; } + + public int LimitSize { get; set; } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Services/GeneralLuis.cs b/skills/src/csharp/experimental/itsmskill/Services/GeneralLuis.cs new file mode 100644 index 0000000000..31fddd23dd --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/GeneralLuis.cs @@ -0,0 +1,88 @@ +// +// Code generated by LUISGen +// Tool github: https://github.com/microsoft/botbuilder-tools +// Changes may cause incorrect behavior and will be lost if the code is +// regenerated. +// +using Newtonsoft.Json; +using System.Collections.Generic; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.AI.Luis; +namespace Luis +{ + public class GeneralLuis: IRecognizerConvert + { + public string Text; + public string AlteredText; + public enum Intent { + Cancel, + Confirm, + Escalate, + FinishTask, + GoBack, + Help, + Logout, + None, + ReadAloud, + Reject, + Repeat, + SelectAny, + SelectItem, + SelectNone, + ShowNext, + ShowPrevious, + StartOver, + Stop + }; + public Dictionary Intents; + + public class _Entities + { + // Simple entities + public string[] DirectionalReference; + + // Built-in entities + public double[] number; + public double[] ordinal; + + // Instance + public class _Instance + { + public InstanceData[] DirectionalReference; + public InstanceData[] number; + public InstanceData[] ordinal; + } + [JsonProperty("$instance")] + public _Instance _instance; + } + public _Entities Entities; + + [JsonExtensionData(ReadData = true, WriteData = true)] + public IDictionary Properties {get; set; } + + public void Convert(dynamic result) + { + var app = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(result)); + Text = app.Text; + AlteredText = app.AlteredText; + Intents = app.Intents; + Entities = app.Entities; + Properties = app.Properties; + } + + public (Intent intent, double score) TopIntent() + { + Intent maxIntent = Intent.None; + var max = 0.0; + foreach (var entry in Intents) + { + if (entry.Value.Score > max) + { + maxIntent = entry.Key; + max = entry.Value.Score.Value; + } + } + return (maxIntent, max); + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs b/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs new file mode 100644 index 0000000000..d85a74baae --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using ITSMSkill.Models; + +namespace ITSMSkill.Services +{ + public interface IITServiceManagement + { + Task CreateTicket(string description, UrgencyLevel urgency); + + // like description & in urgencies & equal id & in states + Task SearchTicket(string description = null, List urgencies = null, string id = null, List states = null); + + Task SearchKnowledge(string query); + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Services/IServiceManager.cs b/skills/src/csharp/experimental/itsmskill/Services/IServiceManager.cs new file mode 100644 index 0000000000..ac0969c062 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/IServiceManager.cs @@ -0,0 +1,9 @@ +using Microsoft.Bot.Schema; + +namespace ITSMSkill.Services +{ + public interface IServiceManager + { + IITServiceManagement CreateManagement(BotSettings botSettings, TokenResponse tokenResponse); + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs new file mode 100644 index 0000000000..e69ab4de4e --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs @@ -0,0 +1,66 @@ +// +// Code generated by LUISGen +// Tool github: https://github.com/microsoft/botbuilder-tools +// Changes may cause incorrect behavior and will be lost if the code is +// regenerated. +// +using Newtonsoft.Json; +using System.Collections.Generic; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.AI.Luis; +namespace Luis +{ + public partial class ITSMLuis: IRecognizerConvert + { + public string Text; + public string AlteredText; + public enum Intent { + None, + TicketCreate + }; + public Dictionary Intents; + + public class _Entities + { + // Simple entities + public string[] TicketDescription; + + // Instance + public class _Instance + { + public InstanceData[] TicketDescription; + } + [JsonProperty("$instance")] + public _Instance _instance; + } + public _Entities Entities; + + [JsonExtensionData(ReadData = true, WriteData = true)] + public IDictionary Properties {get; set; } + + public void Convert(dynamic result) + { + var app = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(result, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); + Text = app.Text; + AlteredText = app.AlteredText; + Intents = app.Intents; + Entities = app.Entities; + Properties = app.Properties; + } + + public (Intent intent, double score) TopIntent() + { + Intent maxIntent = Intent.None; + var max = 0.0; + foreach (var entry in Intents) + { + if (entry.Value.Score > max) + { + maxIntent = entry.Key; + max = entry.Value.Score.Value; + } + } + return (maxIntent, max); + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Services/ServiceManager.cs b/skills/src/csharp/experimental/itsmskill/Services/ServiceManager.cs new file mode 100644 index 0000000000..7cc9c6b906 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceManager.cs @@ -0,0 +1,19 @@ +using Microsoft.Bot.Schema; + +namespace ITSMSkill.Services +{ + public class ServiceManager : IServiceManager + { + public IITServiceManagement CreateManagement(BotSettings botSettings, TokenResponse tokenResponse) + { + if (!string.IsNullOrEmpty(botSettings.ServiceNowUrl) && tokenResponse.ConnectionName == "ServiceNow") + { + return new ServiceNow.Management(botSettings.ServiceNowUrl, tokenResponse.Token, botSettings.LimitSize); + } + else + { + return null; + } + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs new file mode 100644 index 0000000000..1b12b0af51 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using ITSMSkill.Models; +using ITSMSkill.Models.ServiceNow; +using RestSharp; + +namespace ITSMSkill.Services.ServiceNow +{ + public class Management : IITServiceManagement + { + private static readonly string TicketResource = "table/incident"; + private static readonly string KnowledgeResource = "table/kb_knowledge"; + private static readonly Dictionary UrgencyToString; + private static readonly Dictionary StringToUrgency; + private static readonly Dictionary TicketStateToString; + private static readonly Dictionary StringToTicketState; + private readonly RestClient client; + private readonly string token; + private readonly int limitSize; + + static Management() + { + UrgencyToString = new Dictionary() + { + { UrgencyLevel.None, string.Empty }, + { UrgencyLevel.Low, "3" }, + { UrgencyLevel.Medium, "2" }, + { UrgencyLevel.High, "1" } + }; + StringToUrgency = new Dictionary(UrgencyToString.Select(pair => KeyValuePair.Create(pair.Value, pair.Key))); + TicketStateToString = new Dictionary() + { + { TicketState.None, string.Empty }, + { TicketState.New, "1" }, + { TicketState.InProgress, "2" }, + { TicketState.OnHold, "3" }, + { TicketState.Resolved, "6" }, + { TicketState.Closed, "7" }, + { TicketState.Canceled, "8" } + }; + StringToTicketState = new Dictionary(TicketStateToString.Select(pair => KeyValuePair.Create(pair.Value, pair.Key))); + } + + public Management(string url, string token, int limitSize) + { + this.client = new RestClient($"{url}/api/now/v1/"); + this.token = token; + this.limitSize = limitSize; + } + + public async Task CreateTicket(string description, UrgencyLevel urgency) + { + var request = CreateRequest(TicketResource); + var body = new CreateTicketRequest() + { + short_description = description, + urgency = UrgencyToString[urgency] + }; + request.AddJsonBody(body); + try + { + var result = await client.PostAsync(request); + + // didn't find way to get current user's id directly, so update again. or have to create a custom api like https://community.servicenow.com/community?id=community_question&sys_id=52efcb88db1ddb084816f3231f9619c7 + request = CreateRequest($"{TicketResource}/{result.result.sys_id}?sysparm_exclude_ref_link=true"); + var updateBody = new + { + caller_id = result.result.opened_by.value + }; + request.AddJsonBody(updateBody); + result = await client.PatchAsync(request); + + return new CreateTicketResult() + { + Success = true, + Ticket = ConvertTicket(result.result) + }; + } + catch (Exception ex) + { + return new CreateTicketResult() + { + Success = false, + ErrorMessage = ex.Message + }; + } + } + + public async Task SearchTicket(string description = null, List urgencies = null, string id = null, List states = null) + { + var request = CreateRequest(TicketResource); + var sysparmQuery = new List(); + if (!string.IsNullOrEmpty(description)) + { + sysparmQuery.Add($"short_descriptionLIKE{description}"); + } + + if (urgencies != null && urgencies.Count > 0) + { + sysparmQuery.Add($"urgencyIN{string.Join(',', urgencies.Select(urgency => UrgencyToString[urgency]))}"); + } + + if (!string.IsNullOrEmpty(id)) + { + sysparmQuery.Add($"sys_id={id}"); + } + + if (states != null && states.Count > 0) + { + sysparmQuery.Add($"stateIN{string.Join(',', states.Select(state => TicketStateToString[state]))}"); + } + + if (sysparmQuery.Count > 0) + { + request.AddParameter("sysparm_query", string.Join('^', sysparmQuery)); + } + + request.AddParameter("sysparm_limit", limitSize); + + try + { + var result = await client.GetAsync(request); + return new SearchTicketResult() + { + Success = true, + Tickets = result.result?.Select(r => ConvertTicket(r)).ToArray() + }; + } + catch (Exception ex) + { + return new SearchTicketResult() + { + Success = false, + ErrorMessage = ex.Message + }; + } + } + + public async Task SearchKnowledge(string query) + { + var request = CreateRequest(KnowledgeResource); + + // https://codecreative.io/blog/gliderecord-full-text-search-explained/ + request.AddParameter("sysparm_query", $"IR_AND_OR_QUERY={query}"); + + request.AddParameter("sysparm_limit", limitSize); + + try + { + var result = await client.GetAsync(request); + return new SearchKnowledgeResult() + { + Success = true, + Knowledges = result.result?.Select(r => ConvertKnowledge(r)).ToArray() + }; + } + catch (Exception ex) + { + return new SearchKnowledgeResult() + { + Success = false, + ErrorMessage = ex.Message + }; + } + } + + private Ticket ConvertTicket(TicketResponse ticketResponse) + { + var ticket = new Ticket() + { + Id = ticketResponse.sys_id, + Description = ticketResponse.short_description, + Urgency = StringToUrgency[ticketResponse.urgency], + State = StringToTicketState[ticketResponse.state], + OpenedTime = DateTime.Parse(ticketResponse.opened_at) + }; + + if (!string.IsNullOrEmpty(ticketResponse.close_code)) + { + if (!string.IsNullOrEmpty(ticketResponse.close_notes)) + { + ticket.ResolvedReason = $"{ticketResponse.close_code}:\n{ticketResponse.close_notes}"; + } + else + { + ticket.ResolvedReason = ticketResponse.close_code; + } + } + else + { + ticket.ResolvedReason = ticketResponse.close_notes; + } + + return ticket; + } + + private Knowledge ConvertKnowledge(KnowledgeResponse knowledgeResponse) + { + var knowledge = new Knowledge() + { + Id = knowledgeResponse.sys_id, + Title = knowledgeResponse.short_description, + UpdatedTime = DateTime.Parse(knowledgeResponse.sys_updated_on) + }; + if (!string.IsNullOrEmpty(knowledgeResponse.text)) + { + // TODO temporary solution + Regex reg = new Regex("<[^>]+>", RegexOptions.IgnoreCase); + knowledge.Content = reg.Replace(knowledgeResponse.text, string.Empty); + } + else + { + knowledge.Content = knowledgeResponse.wiki; + } + + return knowledge; + } + + private RestRequest CreateRequest(string resource) + { + var request = new RestRequest(resource); + request.AddHeader("Accept", "application/json"); + request.AddHeader("Content-Type", "application/json"); + request.AddHeader("Authorization", $"Bearer {token}"); + return request; + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Startup.cs b/skills/src/csharp/experimental/itsmskill/Startup.cs new file mode 100644 index 0000000000..afb7645ba1 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Startup.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using ITSMSkill.Adapters; +using ITSMSkill.Bots; +using ITSMSkill.Dialogs; +using ITSMSkill.Responses.CreateTicket; +using ITSMSkill.Responses.Main; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Services; +using Microsoft.ApplicationInsights; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.ApplicationInsights; +using Microsoft.Bot.Builder.Azure; +using Microsoft.Bot.Builder.BotFramework; +using Microsoft.Bot.Builder.Integration.ApplicationInsights.Core; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Builder.Solutions; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Builder.Solutions.TaskExtensions; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace ITSMSkill +{ + public class Startup + { + public Startup(IHostingEnvironment env, ILoggerFactory loggerFactory) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddJsonFile("cognitivemodels.json", optional: true) + .AddJsonFile($"cognitivemodels.{env.EnvironmentName}.json", optional: true) + .AddJsonFile("skills.json", optional: true) + .AddJsonFile($"skills.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + + Configuration = builder.Build(); + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + + var provider = services.BuildServiceProvider(); + + // Load settings + var settings = new BotSettings(); + Configuration.Bind(settings); + services.AddSingleton(settings); + services.AddSingleton(settings); + + // Configure credentials + services.AddSingleton(); + services.AddSingleton(new MicrosoftAppCredentials(settings.MicrosoftAppId, settings.MicrosoftAppPassword)); + + // Configure telemetry + services.AddApplicationInsightsTelemetry(); + var telemetryClient = new BotTelemetryClient(new TelemetryClient()); + services.AddSingleton(telemetryClient); + services.AddBotApplicationInsights(telemetryClient); + + // Configure bot services + services.AddSingleton(); + + // Configure storage + // Uncomment the following line for local development without Cosmos Db + // services.AddSingleton(); + services.AddSingleton(new CosmosDbStorage(settings.CosmosDb)); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => + { + var userState = sp.GetService(); + var conversationState = sp.GetService(); + return new BotStateSet(userState, conversationState); + }); + + // Configure proactive + services.AddSingleton(); + services.AddHostedService(); + + // Configure responses + services.AddSingleton(sp => new ResponseManager( + settings.CognitiveModels.Select(l => l.Key).ToArray(), + new MainResponses(), + new CreateTicketResponses(), + new SharedResponses())); + + // Configure service + services.AddSingleton(new ServiceManager()); + + // Register dialogs + services.AddTransient(); + services.AddTransient(); + + // Configure adapters + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Configure bot + services.AddTransient>(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseBotApplicationInsights() + .UseDefaultFiles() + .UseStaticFiles() + .UseWebSockets() + .UseMvc(); + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/appsettings.json b/skills/src/csharp/experimental/itsmskill/appsettings.json new file mode 100644 index 0000000000..488b388ed3 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/appsettings.json @@ -0,0 +1,25 @@ +{ + "microsoftAppId": "", + "microsoftAppPassword": "", + "oauthConnections": [ + { + "provider": "Generic Oauth 2", + "name": "ServiceNow" + } + ], + "ApplicationInsights": { + "InstrumentationKey": "" + }, + "blobStorage": { + "connectionString": "", + "container": "transcripts" + }, + "cosmosDb": { + "authkey": "", + "cosmosDBEndpoint": "", + "collectionId": "botstate-collection", + "databaseId": "botstate-db" + }, + "serviceNowUrl": "https://instance.service-now.com", + "limitSize": 5 +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/cognitivemodels.json b/skills/src/csharp/experimental/itsmskill/cognitivemodels.json new file mode 100644 index 0000000000..40e5289e17 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/cognitivemodels.json @@ -0,0 +1,29 @@ +{ + "defaultLocale": "en-us", + "cognitiveModels": { + "en": { + "languageModels": [ + { + "id": "General", + "name": "", + "appid": "", + "version": "", + "region": "", + "authoringkey": "", + "authoringRegion": "", + "subscriptionkey": "" + }, + { + "id": "ITSM", + "name": "", + "appid": "", + "version": "", + "region": "", + "authoringkey": "", + "authoringRegion": "", + "subscriptionkey": "" + } + ] + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/manifestTemplate.json b/skills/src/csharp/experimental/itsmskill/manifestTemplate.json new file mode 100644 index 0000000000..1dbc6d1024 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/manifestTemplate.json @@ -0,0 +1,37 @@ +{ + "id": "", + "name": "", + "description": "", + "iconUrl": "", + "authenticationConnections": [ + { + "id": "", + "serviceProviderId": "", + "scopes": "" + } + ], + "actions": [ + { + "id": "", + "definition": { + "description": "", + "slots": [ + { + "name": "", + "types": [ "string" ] + } + ], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "luisModel#intent" + ] + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/readme.md b/skills/src/csharp/experimental/itsmskill/readme.md new file mode 100644 index 0000000000..f8ec0388c4 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/readme.md @@ -0,0 +1,12 @@ +# IT Service Managerment Experimental Skill + +To test this skill, one has to setup the following: + +* Create a ServiceNow instance in https://developer.servicenow.com/app.do#!/instance and update the serviceNowUrl of appsettings.json +* Set up endpoint (https://docs.servicenow.com/bundle/london-platform-administration/page/administer/security/task/t_CreateEndpointforExternalClients.html#t_CreateEndpointforExternalClients) for Client id and Client secret in the following OAuth Connection + - Redirect URL is https://token.botframework.com/.auth/web/redirect +* Add an OAuth Connection in the Settings of Web App Bot named 'ServiceNow' with Service Provider 'Generic Oauth 2' + - Authorization URL as https://instance.service-now.com/oauth_auth.do + - Token URL, Refresh URL as https://instance.service-now.com/oauth_token.do + +Once this skill is done, these will be moved into the Experimental Skill [documentation page](/docs/reference/skills/experimental.md). diff --git a/skills/src/csharp/experimental/itsmskill/wwwroot/default.htm b/skills/src/csharp/experimental/itsmskill/wwwroot/default.htm new file mode 100644 index 0000000000..2a99cf4e79 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/wwwroot/default.htm @@ -0,0 +1,426 @@ + + + + + + + IT Service Management Skill Template + + + + + +
+
+
+
IT Service Management Skill Template
+
+
+
+
+
Your IT Service Management Skill is ready!
+
+ You can test your IT Service Management Skill in the Bot Framework Emulator
+ by opening the Emulator and providing the Endpoint shown below along with the Microsoft AppId and Password which you can find in appsettings.json.
+
+ +
+ Your IT Service Management Skill's endpoint URL typically looks + like this: +
+
https://your_bots_hostname/api/messages
+
+
In addition the IT Service Management Skill manifest endpoint can be found here: +
+
https://your_bots_hostname/api/skill/manifest
+
+
+
+
+ +
+ + + \ No newline at end of file From d9437776805538c913fa979d64b73b94df919a5b Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Mon, 12 Aug 2019 17:48:17 +0800 Subject: [PATCH 02/15] [ITSM] add update ticket action --- .../Deployment/Resources/LU/en/ITSM.lu | 13 ++ .../Resources/LU/en/Ticket/Update.lu | 18 ++ .../itsmskill/Dialogs/CreateTicketDialog.cs | 128 +---------- .../itsmskill/Dialogs/MainDialog.cs | 8 + .../itsmskill/Dialogs/SkillDialogBase.cs | 215 ++++++++++++++++++ .../itsmskill/Dialogs/UpdateTicketDialog.cs | 154 +++++++++++++ .../experimental/itsmskill/ITSMSkill.csproj | 12 +- ...CreateTicketResult.cs => AttributeType.cs} | 7 +- ...KnowledgeResult.cs => KnowledgesResult.cs} | 2 +- ...Response.cs => MultiKnowledgesResponse.cs} | 2 +- ...ketResponse.cs => MultiTicketsResponse.cs} | 2 +- ...ketResponse.cs => SingleTicketResponse.cs} | 2 +- .../itsmskill/Models/SkillState.cs | 18 ++ ...SearchTicketResult.cs => TicketsResult.cs} | 2 +- .../itsmskill/Prompts/AttributePrompt.cs | 108 +++++++++ .../Responses/Shared/SharedResponses.cs | 4 + .../Responses/Shared/SharedResponses.json | 38 ++++ .../Shared/SharedStrings.Designer.cs | 27 +++ .../Responses/Shared/SharedStrings.resx | 9 + .../TicketResponses.cs} | 5 +- .../TicketResponses.json} | 9 + .../TicketResponses.tt} | 0 .../Services/IITServiceManagement.cs | 8 +- .../itsmskill/Services/ITSMLuis.cs | 9 +- .../Services/ServiceNow/Management.cs | 75 ++++-- .../csharp/experimental/itsmskill/Startup.cs | 5 +- 26 files changed, 725 insertions(+), 155 deletions(-) create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Update.lu create mode 100644 skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs rename skills/src/csharp/experimental/itsmskill/Models/{CreateTicketResult.cs => AttributeType.cs} (60%) rename skills/src/csharp/experimental/itsmskill/Models/{SearchKnowledgeResult.cs => KnowledgesResult.cs} (79%) rename skills/src/csharp/experimental/itsmskill/Models/ServiceNow/{SearchKnowledgeResponse.cs => MultiKnowledgesResponse.cs} (83%) rename skills/src/csharp/experimental/itsmskill/Models/ServiceNow/{SearchTicketResponse.cs => MultiTicketsResponse.cs} (84%) rename skills/src/csharp/experimental/itsmskill/Models/ServiceNow/{CreateTicketResponse.cs => SingleTicketResponse.cs} (84%) rename skills/src/csharp/experimental/itsmskill/Models/{SearchTicketResult.cs => TicketsResult.cs} (79%) create mode 100644 skills/src/csharp/experimental/itsmskill/Prompts/AttributePrompt.cs rename skills/src/csharp/experimental/itsmskill/Responses/{CreateTicket/CreateTicketResponses.cs => Ticket/TicketResponses.cs} (72%) rename skills/src/csharp/experimental/itsmskill/Responses/{CreateTicket/CreateTicketResponses.json => Ticket/TicketResponses.json} (50%) rename skills/src/csharp/experimental/itsmskill/Responses/{CreateTicket/CreateTicketResponses.tt => Ticket/TicketResponses.tt} (100%) diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu index 4e4c5be9aa..beda20b756 100644 --- a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu @@ -1,4 +1,5 @@ [Ticket Create](./Ticket/Create.lu) +[Ticket Update](./Ticket/Update.lu) # None > from chitchat @@ -18,3 +19,15 @@ > # Entity definitions $TicketDescription:simple + +> # List entities + +$AttributeType:urgency= + +$AttributeType:description= + +$UrgencyLevel:low= + +$UrgencyLevel:medium= + +$UrgencyLevel:high= diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Update.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Update.lu new file mode 100644 index 0000000000..4aeaa9e526 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Update.lu @@ -0,0 +1,18 @@ +# TicketUpdate + +- update a ticket +- update an incident +- update a issue +- update ticket's urgency to high +- update ticket's description tp {TicketDescription=can't log} +- change a ticket +- change an incident +- change a issue +- change ticket's urgency to low +- change ticket's description to {TicketDescription=unable to access} +- i would like to change a ticket +- i would like to change an incident +- i would like to change a issue +- i would like to update a ticket +- i would like to update an incident +- i would like to update a issue diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs index 0208d76286..0a339cece6 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs @@ -1,11 +1,14 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Threading; using System.Threading.Tasks; using ITSMSkill.Models; -using ITSMSkill.Responses.CreateTicket; +using ITSMSkill.Responses.Ticket; using ITSMSkill.Responses.Shared; using ITSMSkill.Services; using Microsoft.Bot.Builder; @@ -43,59 +46,11 @@ public CreateTicketDialog( AfterGetAuthToken, CreateTicket }; - AddDialog(new WaterfallDialog(Actions.CreateTicket, createTicket) { TelemetryClient = telemetryClient }); + AddDialog(new WaterfallDialog(Actions.CreateTicket, createTicket)); InitialDialogId = Actions.CreateTicket; } - public async Task CheckDescription(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) - { - var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - if (string.IsNullOrEmpty(state.TicketDescription)) - { - return await sc.NextAsync(false); - } - else - { - var replacements = new StringDictionary - { - { "Description", state.TicketDescription } - }; - - var options = new PromptOptions() - { - Prompt = ResponseManager.GetResponse(SharedResponses.ConfirmDescription, replacements) - }; - - return await sc.PromptAsync(nameof(ConfirmPrompt), options); - } - } - - public async Task InputDescription(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) - { - var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - if (!(bool)sc.Result || string.IsNullOrEmpty(state.TicketDescription)) - { - var options = new PromptOptions() - { - Prompt = ResponseManager.GetResponse(SharedResponses.InputDescription) - }; - - return await sc.PromptAsync(nameof(TextPrompt), options); - } - else - { - return await sc.NextAsync(state.TicketDescription); - } - } - - public async Task SetDescription(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) - { - var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - state.TicketDescription = (string)sc.Result; - return await sc.NextAsync(); - } - public async Task DisplayExisting(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); @@ -163,73 +118,6 @@ public CreateTicketDialog( } } - public async Task CheckUrgency(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) - { - var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - if (state.UrgencyLevel == UrgencyLevel.None) - { - return await sc.NextAsync(false); - } - else - { - var replacements = new StringDictionary - { - { "Urgency", state.UrgencyLevel.ToString() } - }; - - var options = new PromptOptions() - { - Prompt = ResponseManager.GetResponse(SharedResponses.ConfirmUrgency, replacements) - }; - - return await sc.PromptAsync(nameof(ConfirmPrompt), options); - } - } - - public async Task InputUrgency(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) - { - var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - if (!(bool)sc.Result || state.UrgencyLevel == UrgencyLevel.None) - { - var options = new PromptOptions() - { - Prompt = ResponseManager.GetResponse(SharedResponses.InputUrgency), - Choices = new List() - { - new Choice() - { - Value = UrgencyLevel.Low.ToString() - }, - new Choice() - { - Value = UrgencyLevel.Medium.ToString() - }, - new Choice() - { - Value = UrgencyLevel.High.ToString() - } - } - }; - - return await sc.PromptAsync(nameof(ChoicePrompt), options); - } - else - { - return await sc.NextAsync(new FoundChoice() - { - Value = state.UrgencyLevel.ToString() - }); - } - } - - public async Task SetUrgency(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) - { - var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - Enum.TryParse(((FoundChoice)sc.Result).Value, out UrgencyLevel urgency); - state.UrgencyLevel = urgency; - return await sc.NextAsync(); - } - public async Task CreateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); @@ -255,10 +143,10 @@ public CreateTicketDialog( var card = new Card() { Name = GetDivergedCardName(sc.Context, "Ticket"), - Data = ConvertTicket(result.Ticket) + Data = ConvertTicket(result.Tickets[0]) }; - await sc.Context.SendActivityAsync(ResponseManager.GetCardResponse(CreateTicketResponses.TicketCreated, card, null)); + await sc.Context.SendActivityAsync(ResponseManager.GetCardResponse(TicketResponses.TicketCreated, card, null)); return await sc.NextAsync(); } } diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs index d0642d0d4a..491cfc05ec 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs @@ -39,6 +39,7 @@ public MainDialog( UserState userState, ConversationState conversationState, CreateTicketDialog createTicketDialog, + UpdateTicketDialog updateTicketDialog, IBotTelemetryClient telemetryClient) : base(nameof(MainDialog), telemetryClient) { @@ -53,6 +54,7 @@ public MainDialog( // Register dialogs AddDialog(createTicketDialog); + AddDialog(updateTicketDialog); } protected override async Task OnStartAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) @@ -97,6 +99,12 @@ public MainDialog( break; } + case ITSMLuis.Intent.TicketUpdate: + { + turnResult = await dc.BeginDialogAsync(nameof(UpdateTicketDialog)); + break; + } + case ITSMLuis.Intent.None: { // No intent was identified, send confused message diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs index 4b5c83f89a..7e06f0cd49 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using ITSMSkill.Models; +using ITSMSkill.Prompts; using ITSMSkill.Responses.Shared; using ITSMSkill.Services; using Luis; @@ -51,9 +53,29 @@ public SkillDialogBase( AddDialog(new MultiProviderAuthDialog(settings.OAuthConnections)); + var setDescription = new WaterfallStep[] + { + CheckDescription, + InputDescription, + SetDescription + }; + + var setUrgency = new WaterfallStep[] + { + CheckUrgency, + InputUrgency, + SetUrgency + }; + + var attributesForUpdate = new AttributeType[] { AttributeType.Description, AttributeType.Urgency }; + AddDialog(new TextPrompt(nameof(TextPrompt))); AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt))); AddDialog(new ChoicePrompt(nameof(ChoicePrompt))); + AddDialog(new AttributePrompt(Actions.UpdateAttributeNoYesNo, attributesForUpdate, false)); + AddDialog(new AttributePrompt(Actions.UpdateAttributeHasYesNo, attributesForUpdate, true)); + AddDialog(new WaterfallDialog(Actions.SetDescription, setDescription)); + AddDialog(new WaterfallDialog(Actions.SetUrgency, setUrgency)); } protected BotSettings Settings { get; set; } @@ -120,6 +142,193 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can } } + protected async Task CheckId(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (string.IsNullOrEmpty(state.Id)) + { + return await sc.NextAsync(false); + } + else + { + var replacements = new StringDictionary + { + { "Id", state.Id } + }; + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.ConfirmId, replacements) + }; + + return await sc.PromptAsync(nameof(ConfirmPrompt), options); + } + } + + protected async Task InputId(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (!(bool)sc.Result || string.IsNullOrEmpty(state.Id)) + { + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.InputId) + }; + + return await sc.PromptAsync(nameof(TextPrompt), options); + } + else + { + return await sc.NextAsync(state.Id); + } + } + + protected async Task SetId(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.Id = (string)sc.Result; + return await sc.NextAsync(); + } + + protected async Task CheckAttributeNoConfirm(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (state.AttributeType == AttributeType.None) + { + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.InputAttribute) + }; + + return await sc.PromptAsync(Actions.UpdateAttributeNoYesNo, options); + } + else + { + return await sc.NextAsync(state.AttributeType); + } + } + + protected async Task SetAttribute(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.AttributeType = (AttributeType)sc.Result; + return await sc.NextAsync(); + } + + protected async Task CheckDescription(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (string.IsNullOrEmpty(state.TicketDescription)) + { + return await sc.NextAsync(false); + } + else + { + var replacements = new StringDictionary + { + { "Description", state.TicketDescription } + }; + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.ConfirmDescription, replacements) + }; + + return await sc.PromptAsync(nameof(ConfirmPrompt), options); + } + } + + protected async Task InputDescription(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (!(bool)sc.Result || string.IsNullOrEmpty(state.TicketDescription)) + { + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.InputDescription) + }; + + return await sc.PromptAsync(nameof(TextPrompt), options); + } + else + { + return await sc.NextAsync(state.TicketDescription); + } + } + + protected async Task SetDescription(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.TicketDescription = (string)sc.Result; + return await sc.NextAsync(); + } + + protected async Task CheckUrgency(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (state.UrgencyLevel == UrgencyLevel.None) + { + return await sc.NextAsync(false); + } + else + { + var replacements = new StringDictionary + { + { "Urgency", state.UrgencyLevel.ToString() } + }; + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.ConfirmUrgency, replacements) + }; + + return await sc.PromptAsync(nameof(ConfirmPrompt), options); + } + } + + protected async Task InputUrgency(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (!(bool)sc.Result || state.UrgencyLevel == UrgencyLevel.None) + { + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.InputUrgency), + Choices = new List() + { + new Choice() + { + Value = UrgencyLevel.Low.ToString() + }, + new Choice() + { + Value = UrgencyLevel.Medium.ToString() + }, + new Choice() + { + Value = UrgencyLevel.High.ToString() + } + } + }; + + return await sc.PromptAsync(nameof(ChoicePrompt), options); + } + else + { + return await sc.NextAsync(new FoundChoice() + { + Value = state.UrgencyLevel.ToString() + }); + } + } + + protected async Task SetUrgency(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.UrgencyLevel = Enum.Parse(((FoundChoice)sc.Result).Value); + return await sc.NextAsync(); + } + // Validators protected Task TokenResponseValidator(PromptValidatorContext pc, CancellationToken cancellationToken) { @@ -234,6 +443,12 @@ protected string GetDivergedCardName(ITurnContext turnContext, string card) protected class Actions { public const string CreateTicket = "CreateTicket"; + public const string SetDescription = "SetDescription"; + public const string SetUrgency = "SetUrgency"; + public const string UpdateTicket = "UpdateTicket"; + public const string UpdateAttribute = "UpdateAttribute"; + public const string UpdateAttributeNoYesNo = "UpdateAttributeNoYesNo"; + public const string UpdateAttributeHasYesNo = "UpdateAttributeHasYesNo"; } } } \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs new file mode 100644 index 0000000000..d8e3d64253 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Specialized; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ITSMSkill.Models; +using ITSMSkill.Responses.Ticket; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Services; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Dialogs.Choices; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Connector; +using Microsoft.Bot.Schema; + +namespace ITSMSkill.Dialogs +{ + public class UpdateTicketDialog : SkillDialogBase + { + public UpdateTicketDialog( + BotSettings settings, + BotServices services, + ResponseManager responseManager, + ConversationState conversationState, + IServiceManager serviceManager, + IBotTelemetryClient telemetryClient) + : base(nameof(UpdateTicketDialog), settings, services, responseManager, conversationState, serviceManager, telemetryClient) + { + var updateTicket = new WaterfallStep[] + { + CheckId, + InputId, + SetId, + UpdateAttributeLoop, + GetAuthToken, + AfterGetAuthToken, + UpdateTicket + }; + + var updateAttribute = new WaterfallStep[] + { + CheckAttributeNoConfirm, + SetAttribute, + UpdateSelectedAttribute, + UpdateMore, + AfterUpdateMore + }; + + AddDialog(new WaterfallDialog(Actions.UpdateTicket, updateTicket) { TelemetryClient = telemetryClient }); + + AddDialog(new WaterfallDialog(Actions.UpdateAttribute, updateAttribute) { TelemetryClient = telemetryClient }); + + InitialDialogId = Actions.UpdateTicket; + } + + protected async Task UpdateAttributeLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + return await sc.BeginDialogAsync(Actions.UpdateAttribute); + } + + protected async Task UpdateSelectedAttribute(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (state.AttributeType == AttributeType.Description) + { + return await sc.BeginDialogAsync(Actions.SetDescription); + } + else if (state.AttributeType == AttributeType.Urgency) + { + return await sc.BeginDialogAsync(Actions.SetUrgency); + } + else + { + throw new Exception($"Invalid AttributeType: {state.AttributeType}"); + } + } + + protected async Task UpdateMore(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.InputAttributeMore) + }; + + return await sc.PromptAsync(Actions.UpdateAttributeHasYesNo, options); + } + + protected async Task AfterUpdateMore(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + if (sc.Result == null) + { + return await sc.NextAsync(); + } + + var type = (AttributeType)sc.Result; + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.AttributeType = type; + + if (state.AttributeType == AttributeType.Description) + { + state.TicketDescription = null; + } + else if (state.AttributeType == AttributeType.Urgency) + { + state.UrgencyLevel = UrgencyLevel.None; + } + else if (state.AttributeType == AttributeType.None) + { + } + else + { + throw new Exception($"Invalid AttributeType: {state.AttributeType}"); + } + + return await sc.ReplaceDialogAsync(Actions.UpdateAttribute); + } + + protected async Task UpdateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (state.Token == null) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); + return await sc.EndDialogAsync(); + } + + var management = ServiceManager.CreateManagement(Settings, state.Token); + var result = await management.UpdateTicket(state.Id, state.TicketDescription, state.UrgencyLevel); + + if (!result.Success) + { + var errorReplacements = new StringDictionary + { + { "Error", result.ErrorMessage } + }; + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); + return await sc.EndDialogAsync(); + } + + var card = new Card() + { + Name = GetDivergedCardName(sc.Context, "Ticket"), + Data = ConvertTicket(result.Tickets[0]) + }; + + await sc.Context.SendActivityAsync(ResponseManager.GetCardResponse(TicketResponses.TicketUpdated, card, null)); + return await sc.NextAsync(); + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj index 71703eb6cb..df61d3473e 100644 --- a/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj +++ b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj @@ -10,7 +10,7 @@ - + @@ -19,7 +19,7 @@ - + @@ -60,9 +60,9 @@ - + TextTemplatingFileGenerator - CreateTicketResponses.cs + TicketResponses.cs TextTemplatingFileGenerator @@ -87,10 +87,10 @@ - + True True - CreateTicketResponses.tt + TicketResponses.tt True diff --git a/skills/src/csharp/experimental/itsmskill/Models/CreateTicketResult.cs b/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs similarity index 60% rename from skills/src/csharp/experimental/itsmskill/Models/CreateTicketResult.cs rename to skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs index 39298903de..4a540226c2 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/CreateTicketResult.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs @@ -5,8 +5,11 @@ namespace ITSMSkill.Models { - public class CreateTicketResult : ResultBase + public enum AttributeType { - public Ticket Ticket { get; set; } + None, + Id, + Description, + Urgency } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/SearchKnowledgeResult.cs b/skills/src/csharp/experimental/itsmskill/Models/KnowledgesResult.cs similarity index 79% rename from skills/src/csharp/experimental/itsmskill/Models/SearchKnowledgeResult.cs rename to skills/src/csharp/experimental/itsmskill/Models/KnowledgesResult.cs index 92db908209..b1003118a8 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/SearchKnowledgeResult.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/KnowledgesResult.cs @@ -5,7 +5,7 @@ namespace ITSMSkill.Models { - public class SearchKnowledgeResult : ResultBase + public class KnowledgesResult : ResultBase { public Knowledge[] Knowledges { get; set; } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchKnowledgeResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiKnowledgesResponse.cs similarity index 83% rename from skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchKnowledgeResponse.cs rename to skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiKnowledgesResponse.cs index 16bf63b363..b64bc65307 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchKnowledgeResponse.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiKnowledgesResponse.cs @@ -5,7 +5,7 @@ namespace ITSMSkill.Models.ServiceNow { - public class SearchKnowledgeResponse + public class MultiKnowledgesResponse { public List result { get; set; } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchTicketResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiTicketsResponse.cs similarity index 84% rename from skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchTicketResponse.cs rename to skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiTicketsResponse.cs index 10e1cf4c7c..4d64e4518e 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SearchTicketResponse.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiTicketsResponse.cs @@ -5,7 +5,7 @@ namespace ITSMSkill.Models.ServiceNow { - public class SearchTicketResponse + public class MultiTicketsResponse { public List result { get; set; } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SingleTicketResponse.cs similarity index 84% rename from skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketResponse.cs rename to skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SingleTicketResponse.cs index 5f94a3eb3d..4a857dfd9c 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketResponse.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SingleTicketResponse.cs @@ -5,7 +5,7 @@ namespace ITSMSkill.Models.ServiceNow { - public class CreateTicketResponse + public class SingleTicketResponse { public TicketResponse result { get; set; } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs index ae6a387f57..5d53651cf6 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using ITSMSkill.Services; using Luis; using Microsoft.Bot.Schema; @@ -16,24 +17,41 @@ public SkillState() public TokenResponse Token { get; set; } + public string Id { get; set; } + public string TicketDescription { get; set; } public UrgencyLevel UrgencyLevel { get; set; } + public AttributeType AttributeType { get; set; } + public void DigestLuisResult(ITSMLuis luis) { Clear(); + if (luis.Entities.TicketDescription != null) { TicketDescription = string.Join(' ', luis.Entities.TicketDescription); } + + if (luis.Entities.UrgencyLevel != null) + { + UrgencyLevel = Enum.Parse(luis.Entities.UrgencyLevel[0][0], true); + } + + if (luis.Entities.AttributeType != null) + { + AttributeType = Enum.Parse(luis.Entities.AttributeType[0][0], true); + } } public void Clear() { Token = null; + Id = null; TicketDescription = null; UrgencyLevel = UrgencyLevel.None; + AttributeType = AttributeType.None; } } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/SearchTicketResult.cs b/skills/src/csharp/experimental/itsmskill/Models/TicketsResult.cs similarity index 79% rename from skills/src/csharp/experimental/itsmskill/Models/SearchTicketResult.cs rename to skills/src/csharp/experimental/itsmskill/Models/TicketsResult.cs index e635b9a22a..3d5fdbe623 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/SearchTicketResult.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/TicketsResult.cs @@ -5,7 +5,7 @@ namespace ITSMSkill.Models { - public class SearchTicketResult : ResultBase + public class TicketsResult : ResultBase { public Ticket[] Tickets { get; set; } } diff --git a/skills/src/csharp/experimental/itsmskill/Prompts/AttributePrompt.cs b/skills/src/csharp/experimental/itsmskill/Prompts/AttributePrompt.cs new file mode 100644 index 0000000000..cfa7db1528 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Prompts/AttributePrompt.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using ITSMSkill.Models; +using ITSMSkill.Responses.Shared; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Solutions.Util; +using Microsoft.Bot.Schema; + +namespace ITSMSkill.Prompts +{ + public class AttributePrompt : Prompt + { + private readonly bool hasYesNo; + private readonly AttributeType[] attributes; + + public AttributePrompt(string dialogId, AttributeType[] attributes, bool hasYesNo, PromptValidator validator = null, string defaultLocale = null) + : base(dialogId, validator) + { + this.hasYesNo = hasYesNo; + this.attributes = attributes; + DefaultLocale = defaultLocale; + } + + public string DefaultLocale { get; set; } + + protected override async Task OnPromptAsync(ITurnContext turnContext, IDictionary state, PromptOptions options, bool isRetry, CancellationToken cancellationToken = default(CancellationToken)) + { + if (turnContext == null) + { + throw new ArgumentNullException(nameof(turnContext)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (isRetry && options.RetryPrompt != null) + { + await turnContext.SendActivityAsync(options.RetryPrompt, cancellationToken).ConfigureAwait(false); + } + else if (options.Prompt != null) + { + await turnContext.SendActivityAsync(options.Prompt, cancellationToken).ConfigureAwait(false); + } + } + + protected override async Task> OnRecognizeAsync(ITurnContext turnContext, IDictionary state, PromptOptions options, CancellationToken cancellationToken = default(CancellationToken)) + { + if (turnContext == null) + { + throw new ArgumentNullException(nameof(turnContext)); + } + + var result = new PromptRecognizerResult(); + if (turnContext.Activity.Type == ActivityTypes.Message) + { + var message = turnContext.Activity.AsMessageActivity(); + + if (hasYesNo) + { + var promptRecognizerResult = ConfirmRecognizerHelper.ConfirmYesOrNo(message.Text, turnContext.Activity.Locale); + if (promptRecognizerResult.Succeeded) + { + result.Succeeded = true; + if (promptRecognizerResult.Value) + { + result.Value = AttributeType.None; + } + else + { + result.Value = null; + } + } + } + + if (!result.Succeeded) + { + var text = message.Text.ToLowerInvariant(); + foreach (var attribute in attributes) + { + if (IsMessageAttributeMatch(text, attribute)) + { + result.Succeeded = true; + result.Value = attribute; + break; + } + } + } + } + + return await Task.FromResult(result); + } + + private bool IsMessageAttributeMatch(string message, AttributeType attribute) + { + switch (attribute) + { + case AttributeType.Description: return message.Equals(SharedStrings.AttributeDescription); + case AttributeType.Urgency: return message.Equals(SharedStrings.AttributeUrgency); + default: return false; + } + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs index eba00dbe9b..729c2db18a 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs @@ -22,6 +22,10 @@ public class SharedResponses : IResponseIdCollection public const string InputDescription = "InputDescription"; public const string ConfirmUrgency = "ConfirmUrgency"; public const string InputUrgency = "InputUrgency"; + public const string ConfirmId = "ConfirmId"; + public const string InputId = "InputId"; + public const string InputAttribute = "InputAttribute"; + public const string InputAttributeMore = "InputAttributeMore"; public const string IfExistingSolve = "IfExistingSolve"; public const string ExistingSolve = "ExistingSolve"; public const string ServiceFailed = "ServiceFailed"; diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json index 0e67659285..8f91268bbd 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json @@ -181,6 +181,44 @@ ], "inputHint": "expectingInput" }, + "ConfirmId": { + "replies": [ + { + "text": "Is the ID \"{Id}\" correct?", + "speak": "Is the ID \"{Id}\" correct?" + } + ], + "inputHint": "expectingInput" + }, + "InputId": { + "replies": [ + { + "text": "Please input the ID:", + "speak": "Please input the ID:" + } + ], + "inputHint": "expectingInput" + }, + "InputAttribute": { + // same with SharedStrings + "suggestedActions": [ "description", "urgency" ], + "replies": [ + { + "text": "Please input the attribute to update:", + "speak": "Please input the attribute to update:" + } + ], + "inputHint": "expectingInput" + }, + "InputAttributeMore": { + "replies": [ + { + "text": "Do you want to update more attribute? Or input the attribute to update directly.", + "speak": "Do you want to update more attribute? Or input the attribute to update directly." + } + ], + "inputHint": "expectingInput" + }, "IfExistingSolve": { "replies": [ { diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs index f54d16094f..60adbec4df 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs @@ -60,6 +60,33 @@ internal SharedStrings() { } } + /// + /// Looks up a localized string similar to description. + /// + public static string AttributeDescription { + get { + return ResourceManager.GetString("AttributeDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to id. + /// + public static string AttributeId { + get { + return ResourceManager.GetString("AttributeId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to urgency. + /// + public static string AttributeUrgency { + get { + return ResourceManager.GetString("AttributeUrgency", resourceCulture); + } + } + /// /// Looks up a localized string similar to Opened at . /// diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx index 65ed63c9b3..f6e4d96411 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx @@ -144,6 +144,15 @@ Resolved + + description + + + id + + + urgency + Opened at diff --git a/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs similarity index 72% rename from skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.cs rename to skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs index cc3c83c0b1..90c6f4de48 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs @@ -4,14 +4,15 @@ using Microsoft.Bot.Builder.Solutions.Responses; -namespace ITSMSkill.Responses.CreateTicket +namespace ITSMSkill.Responses.Ticket { /// /// Contains bot responses. /// - public class CreateTicketResponses : IResponseIdCollection + public class TicketResponses : IResponseIdCollection { // Generated accessors public const string TicketCreated = "TicketCreated"; + public const string TicketUpdated = "TicketUpdated"; } } \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json similarity index 50% rename from skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.json rename to skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json index b283d76289..7dd46db934 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json @@ -7,5 +7,14 @@ } ], "inputHint": "acceptingInput" + }, + "TicketUpdated": { + "replies": [ + { + "text": "Your ticket has been updated. Let me know when you need my help.", + "speak": "Your ticket has been updated. Let me know when you need my help." + } + ], + "inputHint": "acceptingInput" } } diff --git a/skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.tt b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.tt similarity index 100% rename from skills/src/csharp/experimental/itsmskill/Responses/CreateTicket/CreateTicketResponses.tt rename to skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.tt diff --git a/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs b/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs index d85a74baae..bb86aae52f 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs @@ -6,11 +6,13 @@ namespace ITSMSkill.Services { public interface IITServiceManagement { - Task CreateTicket(string description, UrgencyLevel urgency); + Task CreateTicket(string description, UrgencyLevel urgency); // like description & in urgencies & equal id & in states - Task SearchTicket(string description = null, List urgencies = null, string id = null, List states = null); + Task SearchTicket(string description = null, List urgencies = null, string id = null, List states = null); - Task SearchKnowledge(string query); + Task UpdateTicket(string id, string description = null, UrgencyLevel urgency = UrgencyLevel.None); + + Task SearchKnowledge(string query); } } diff --git a/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs index e69ab4de4e..64f7c01450 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs @@ -16,7 +16,8 @@ public partial class ITSMLuis: IRecognizerConvert public string AlteredText; public enum Intent { None, - TicketCreate + TicketCreate, + TicketUpdate }; public Dictionary Intents; @@ -25,10 +26,16 @@ public class _Entities // Simple entities public string[] TicketDescription; + // Lists + public string[][] AttributeType; + public string[][] UrgencyLevel; + // Instance public class _Instance { public InstanceData[] TicketDescription; + public InstanceData[] AttributeType; + public InstanceData[] UrgencyLevel; } [JsonProperty("$instance")] public _Instance _instance; diff --git a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs index 1b12b0af51..7b59e40b39 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs @@ -6,7 +6,9 @@ using System.Threading.Tasks; using ITSMSkill.Models; using ITSMSkill.Models.ServiceNow; +using Newtonsoft.Json; using RestSharp; +using RestSharp.Serializers; namespace ITSMSkill.Services.ServiceNow { @@ -52,7 +54,7 @@ public Management(string url, string token, int limitSize) this.limitSize = limitSize; } - public async Task CreateTicket(string description, UrgencyLevel urgency) + public async Task CreateTicket(string description, UrgencyLevel urgency) { var request = CreateRequest(TicketResource); var body = new CreateTicketRequest() @@ -63,7 +65,7 @@ public async Task CreateTicket(string description, UrgencyLe request.AddJsonBody(body); try { - var result = await client.PostAsync(request); + var result = await client.PostAsync(request); // didn't find way to get current user's id directly, so update again. or have to create a custom api like https://community.servicenow.com/community?id=community_question&sys_id=52efcb88db1ddb084816f3231f9619c7 request = CreateRequest($"{TicketResource}/{result.result.sys_id}?sysparm_exclude_ref_link=true"); @@ -72,17 +74,17 @@ public async Task CreateTicket(string description, UrgencyLe caller_id = result.result.opened_by.value }; request.AddJsonBody(updateBody); - result = await client.PatchAsync(request); + result = await client.PatchAsync(request); - return new CreateTicketResult() + return new TicketsResult() { Success = true, - Ticket = ConvertTicket(result.result) + Tickets = new Ticket[] { ConvertTicket(result.result) } }; } catch (Exception ex) { - return new CreateTicketResult() + return new TicketsResult() { Success = false, ErrorMessage = ex.Message @@ -90,7 +92,7 @@ public async Task CreateTicket(string description, UrgencyLe } } - public async Task SearchTicket(string description = null, List urgencies = null, string id = null, List states = null) + public async Task SearchTicket(string description = null, List urgencies = null, string id = null, List states = null) { var request = CreateRequest(TicketResource); var sysparmQuery = new List(); @@ -123,8 +125,8 @@ public async Task SearchTicket(string description = null, Li try { - var result = await client.GetAsync(request); - return new SearchTicketResult() + var result = await client.GetAsync(request); + return new TicketsResult() { Success = true, Tickets = result.result?.Select(r => ConvertTicket(r)).ToArray() @@ -132,7 +134,7 @@ public async Task SearchTicket(string description = null, Li } catch (Exception ex) { - return new SearchTicketResult() + return new TicketsResult() { Success = false, ErrorMessage = ex.Message @@ -140,7 +142,37 @@ public async Task SearchTicket(string description = null, Li } } - public async Task SearchKnowledge(string query) + public async Task UpdateTicket(string id, string description = null, UrgencyLevel urgency = UrgencyLevel.None) + { + var request = CreateRequest($"{TicketResource}/{id}?sysparm_exclude_ref_link=true"); + var body = new CreateTicketRequest() + { + short_description = description, + urgency = urgency == UrgencyLevel.None ? null : UrgencyToString[urgency] + }; + request.JsonSerializer = new JsonNoNull(); + request.AddJsonBody(body); + try + { + var result = await client.PatchAsync(request); + + return new TicketsResult() + { + Success = true, + Tickets = new Ticket[] { ConvertTicket(result.result) } + }; + } + catch (Exception ex) + { + return new TicketsResult() + { + Success = false, + ErrorMessage = ex.Message + }; + } + } + + public async Task SearchKnowledge(string query) { var request = CreateRequest(KnowledgeResource); @@ -151,8 +183,8 @@ public async Task SearchKnowledge(string query) try { - var result = await client.GetAsync(request); - return new SearchKnowledgeResult() + var result = await client.GetAsync(request); + return new KnowledgesResult() { Success = true, Knowledges = result.result?.Select(r => ConvertKnowledge(r)).ToArray() @@ -160,7 +192,7 @@ public async Task SearchKnowledge(string query) } catch (Exception ex) { - return new SearchKnowledgeResult() + return new KnowledgesResult() { Success = false, ErrorMessage = ex.Message @@ -228,5 +260,20 @@ private RestRequest CreateRequest(string resource) request.AddHeader("Authorization", $"Bearer {token}"); return request; } + + private class JsonNoNull : ISerializer + { + public JsonNoNull() + { + ContentType = "application/json"; + } + + public string ContentType { get; set; } + + public string Serialize(object obj) + { + return JsonConvert.SerializeObject(obj, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + } + } } } diff --git a/skills/src/csharp/experimental/itsmskill/Startup.cs b/skills/src/csharp/experimental/itsmskill/Startup.cs index afb7645ba1..db36d8154f 100644 --- a/skills/src/csharp/experimental/itsmskill/Startup.cs +++ b/skills/src/csharp/experimental/itsmskill/Startup.cs @@ -5,7 +5,7 @@ using ITSMSkill.Adapters; using ITSMSkill.Bots; using ITSMSkill.Dialogs; -using ITSMSkill.Responses.CreateTicket; +using ITSMSkill.Responses.Ticket; using ITSMSkill.Responses.Main; using ITSMSkill.Responses.Shared; using ITSMSkill.Services; @@ -96,7 +96,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(sp => new ResponseManager( settings.CognitiveModels.Select(l => l.Key).ToArray(), new MainResponses(), - new CreateTicketResponses(), + new TicketResponses(), new SharedResponses())); // Configure service @@ -104,6 +104,7 @@ public void ConfigureServices(IServiceCollection services) // Register dialogs services.AddTransient(); + services.AddTransient(); services.AddTransient(); // Configure adapters From eebb7f1334cb9306a798a7fbea9b51aa83be7789 Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Tue, 13 Aug 2019 12:03:01 +0800 Subject: [PATCH 03/15] [ITSM] add show ticket action --- .../itsmskill/Content/Knowledge.1.0.json | 2 +- .../itsmskill/Content/Knowledge.json | 2 +- .../itsmskill/Content/Ticket.1.0.json | 2 +- .../itsmskill/Content/Ticket.json | 2 +- .../Deployment/Resources/LU/en/ITSM.lu | 1 + .../Deployment/Resources/LU/en/Ticket/Show.lu | 17 ++ .../itsmskill/Dialogs/CreateTicketDialog.cs | 2 +- .../itsmskill/Dialogs/MainDialog.cs | 8 + .../itsmskill/Dialogs/ShowTicketDialog.cs | 161 ++++++++++++++++++ .../itsmskill/Dialogs/SkillDialogBase.cs | 103 ++++++++++- .../itsmskill/Dialogs/UpdateTicketDialog.cs | 70 ++------ .../Models/ServiceNow/CreateTicketRequest.cs | 2 + .../Models/ServiceNow/GetUserIdResponse.cs | 12 ++ .../itsmskill/Models/SkillState.cs | 1 + .../Responses/Shared/SharedResponses.cs | 2 - .../Responses/Shared/SharedResponses.json | 20 --- .../Shared/SharedStrings.Designer.cs | 18 ++ .../Responses/Shared/SharedStrings.resx | 6 + .../Responses/Ticket/TicketResponses.cs | 8 + .../Responses/Ticket/TicketResponses.json | 76 +++++++++ .../itsmskill/Services/BotSettings.cs | 2 + .../itsmskill/Services/ITSMLuis.cs | 3 +- .../itsmskill/Services/ServiceManager.cs | 4 +- .../Services/ServiceNow/Management.cs | 84 ++++----- .../csharp/experimental/itsmskill/Startup.cs | 1 + .../experimental/itsmskill/appsettings.json | 1 + .../csharp/experimental/itsmskill/readme.md | 2 + 27 files changed, 473 insertions(+), 139 deletions(-) create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Show.lu create mode 100644 skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Models/ServiceNow/GetUserIdResponse.cs diff --git a/skills/src/csharp/experimental/itsmskill/Content/Knowledge.1.0.json b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.1.0.json index 4f2d1a459e..62835c0d8a 100644 --- a/skills/src/csharp/experimental/itsmskill/Content/Knowledge.1.0.json +++ b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.1.0.json @@ -35,7 +35,7 @@ "type": "TextBlock", "size": "Small", "color": "Default", - "text": "ID: {Id}" + "text": "{Id}" } ] }, diff --git a/skills/src/csharp/experimental/itsmskill/Content/Knowledge.json b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.json index 4a23df3ca8..4559df1887 100644 --- a/skills/src/csharp/experimental/itsmskill/Content/Knowledge.json +++ b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.json @@ -51,7 +51,7 @@ "type": "TextBlock", "size": "Small", "color": "Light", - "text": "ID: {Id}" + "text": "{Id}" } ] }, diff --git a/skills/src/csharp/experimental/itsmskill/Content/Ticket.1.0.json b/skills/src/csharp/experimental/itsmskill/Content/Ticket.1.0.json index 0988fd6ba8..7a309db31a 100644 --- a/skills/src/csharp/experimental/itsmskill/Content/Ticket.1.0.json +++ b/skills/src/csharp/experimental/itsmskill/Content/Ticket.1.0.json @@ -64,7 +64,7 @@ "type": "TextBlock", "size": "Small", "color": "Default", - "text": "ID: {Id}" + "text": "{Id}" } ] }, diff --git a/skills/src/csharp/experimental/itsmskill/Content/Ticket.json b/skills/src/csharp/experimental/itsmskill/Content/Ticket.json index 5d202690d4..376767eebf 100644 --- a/skills/src/csharp/experimental/itsmskill/Content/Ticket.json +++ b/skills/src/csharp/experimental/itsmskill/Content/Ticket.json @@ -79,7 +79,7 @@ "type": "TextBlock", "size": "Small", "color": "Light", - "text": "ID: {Id}" + "text": "{Id}" } ] }, diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu index beda20b756..8ec4754740 100644 --- a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu @@ -1,5 +1,6 @@ [Ticket Create](./Ticket/Create.lu) [Ticket Update](./Ticket/Update.lu) +[Ticket Show](./Ticket/Show.lu) # None > from chitchat diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Show.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Show.lu new file mode 100644 index 0000000000..ddfc6f0b31 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Show.lu @@ -0,0 +1,17 @@ +# TicketShow + +- show my tickets +- show my issues +- show my incidents +- show my urgency high tickets +- show my tickets about {TicketDescription=lost connection} +- view my tickets +- view my issues +- view my incidents +- view my urgency low tickets +- view my tickets about {TicketDescription=new employee hire} +- i would like to view my tickets +- i would like to have a look at my tickets +- i would like to see my tickets +- i would like to see urgency low issues +- i would like to see incidents about {TicketDescription=unable to connect} diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs index 0a339cece6..2692edaf74 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs @@ -8,8 +8,8 @@ using System.Threading; using System.Threading.Tasks; using ITSMSkill.Models; -using ITSMSkill.Responses.Ticket; using ITSMSkill.Responses.Shared; +using ITSMSkill.Responses.Ticket; using ITSMSkill.Services; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs index 491cfc05ec..e205855e66 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs @@ -40,6 +40,7 @@ public MainDialog( ConversationState conversationState, CreateTicketDialog createTicketDialog, UpdateTicketDialog updateTicketDialog, + ShowTicketDialog showTicketDialog, IBotTelemetryClient telemetryClient) : base(nameof(MainDialog), telemetryClient) { @@ -55,6 +56,7 @@ public MainDialog( // Register dialogs AddDialog(createTicketDialog); AddDialog(updateTicketDialog); + AddDialog(showTicketDialog); } protected override async Task OnStartAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) @@ -105,6 +107,12 @@ public MainDialog( break; } + case ITSMLuis.Intent.TicketShow: + { + turnResult = await dc.BeginDialogAsync(nameof(ShowTicketDialog)); + break; + } + case ITSMLuis.Intent.None: { // No intent was identified, send confused message diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs new file mode 100644 index 0000000000..f7a5c23b63 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ITSMSkill.Models; +using ITSMSkill.Prompts; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Responses.Ticket; +using ITSMSkill.Services; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Dialogs.Choices; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Connector; +using Microsoft.Bot.Schema; + +namespace ITSMSkill.Dialogs +{ + public class ShowTicketDialog : SkillDialogBase + { + public ShowTicketDialog( + BotSettings settings, + BotServices services, + ResponseManager responseManager, + ConversationState conversationState, + IServiceManager serviceManager, + IBotTelemetryClient telemetryClient) + : base(nameof(ShowTicketDialog), settings, services, responseManager, conversationState, serviceManager, telemetryClient) + { + var showTicket = new WaterfallStep[] + { + ShowAttributeLoop, + GetAuthToken, + AfterGetAuthToken, + ShowTicket + }; + + var showAttribute = new WaterfallStep[] + { + ShowConstraints, + UpdateMore, + AfterUpdateMore, + CheckAttributeNoConfirm, + SetAttribute, + UpdateSelectedAttribute + }; + + var attributesForShow = new AttributeType[] { AttributeType.Id, AttributeType.Description, AttributeType.Urgency }; + + AddDialog(new WaterfallDialog(Actions.ShowTicket, showTicket) { TelemetryClient = telemetryClient }); + AddDialog(new WaterfallDialog(Actions.ShowAttribute, showAttribute) { TelemetryClient = telemetryClient }); + AddDialog(new AttributePrompt(Actions.ShowAttributeNoYesNo, attributesForShow, false)); + AddDialog(new AttributePrompt(Actions.ShowAttributeHasYesNo, attributesForShow, true)); + + InitialDialogId = Actions.ShowTicket; + + InputAttributeResponse = TicketResponses.ShowAttribute; + InputAttributePrompt = Actions.ShowAttributeNoYesNo; + InputMoreAttributeResponse = TicketResponses.ShowAttributeMore; + InputMoreAttributePrompt = Actions.ShowAttributeHasYesNo; + } + + protected async Task ShowAttributeLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + return await sc.BeginDialogAsync(Actions.ShowAttribute); + } + + protected async Task ShowConstraints(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + var sb = new StringBuilder(); + if (!string.IsNullOrEmpty(state.Id)) + { + sb.AppendLine($"{SharedStrings.ID}{state.Id}"); + } + + if (!string.IsNullOrEmpty(state.TicketDescription)) + { + sb.AppendLine($"{SharedStrings.Description}{state.TicketDescription}"); + } + + if (state.UrgencyLevel != UrgencyLevel.None) + { + sb.AppendLine($"{SharedStrings.Urgency}{ConvertUrgencyLevel(state.UrgencyLevel)}"); + } + + if (sb.Length == 0) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.ShowConstraintNone)); + } + else + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.ShowConstraints)); + await sc.Context.SendActivityAsync(sb.ToString()); + } + + return await sc.NextAsync(); + } + + protected async Task ShowTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (state.Token == null) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); + return await sc.EndDialogAsync(); + } + + var management = ServiceManager.CreateManagement(Settings, state.Token); + var urgencies = new List(); + if (state.UrgencyLevel != UrgencyLevel.None) + { + urgencies.Add(state.UrgencyLevel); + } + + var result = await management.SearchTicket(description: state.TicketDescription, urgencies: urgencies, id: state.Id); + + if (!result.Success) + { + var errorReplacements = new StringDictionary + { + { "Error", result.ErrorMessage } + }; + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); + return await sc.EndDialogAsync(); + } + + if (result.Tickets == null || result.Tickets.Length == 0) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.TicketShowNone)); + return await sc.NextAsync(); + } + else + { + var cards = new List(); + foreach (var ticket in result.Tickets) + { + cards.Add(new Card() + { + Name = GetDivergedCardName(sc.Context, "Ticket"), + Data = ConvertTicket(ticket) + }); + } + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetCardResponse(TicketResponses.TicketShow, cards) + }; + + await sc.Context.SendActivityAsync(ResponseManager.GetCardResponse(TicketResponses.TicketShow, cards)); + return await sc.NextAsync(); + } + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs index 7e06f0cd49..bff4a0bc16 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs @@ -67,15 +67,19 @@ public SkillDialogBase( SetUrgency }; - var attributesForUpdate = new AttributeType[] { AttributeType.Description, AttributeType.Urgency }; + var setId = new WaterfallStep[] + { + CheckId, + InputId, + SetId + }; AddDialog(new TextPrompt(nameof(TextPrompt))); AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt))); AddDialog(new ChoicePrompt(nameof(ChoicePrompt))); - AddDialog(new AttributePrompt(Actions.UpdateAttributeNoYesNo, attributesForUpdate, false)); - AddDialog(new AttributePrompt(Actions.UpdateAttributeHasYesNo, attributesForUpdate, true)); AddDialog(new WaterfallDialog(Actions.SetDescription, setDescription)); AddDialog(new WaterfallDialog(Actions.SetUrgency, setUrgency)); + AddDialog(new WaterfallDialog(Actions.SetId, setId)); } protected BotSettings Settings { get; set; } @@ -88,6 +92,14 @@ public SkillDialogBase( protected IServiceManager ServiceManager { get; set; } + protected string InputAttributeResponse { get; set; } + + protected string InputAttributePrompt { get; set; } + + protected string InputMoreAttributeResponse { get; set; } + + protected string InputMoreAttributePrompt { get; set; } + protected override async Task OnBeginDialogAsync(DialogContext dc, object options, CancellationToken cancellationToken = default(CancellationToken)) { return await base.OnBeginDialogAsync(dc, options, cancellationToken); @@ -197,10 +209,10 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can { var options = new PromptOptions() { - Prompt = ResponseManager.GetResponse(SharedResponses.InputAttribute) + Prompt = ResponseManager.GetResponse(InputAttributeResponse) }; - return await sc.PromptAsync(Actions.UpdateAttributeNoYesNo, options); + return await sc.PromptAsync(InputAttributePrompt, options); } else { @@ -215,6 +227,71 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can return await sc.NextAsync(); } + protected async Task UpdateSelectedAttribute(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (state.AttributeType == AttributeType.Description) + { + return await sc.BeginDialogAsync(Actions.SetDescription); + } + else if (state.AttributeType == AttributeType.Urgency) + { + return await sc.BeginDialogAsync(Actions.SetUrgency); + } + else if (state.AttributeType == AttributeType.Id) + { + return await sc.BeginDialogAsync(Actions.SetId); + } + else + { + throw new Exception($"Invalid AttributeType: {state.AttributeType}"); + } + } + + protected async Task UpdateMore(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(InputMoreAttributeResponse) + }; + + return await sc.PromptAsync(InputMoreAttributePrompt, options); + } + + protected async Task AfterUpdateMore(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + if (sc.Result == null) + { + return await sc.EndDialogAsync(); + } + + var type = (AttributeType)sc.Result; + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.AttributeType = type; + + if (state.AttributeType == AttributeType.Description) + { + state.TicketDescription = null; + } + else if (state.AttributeType == AttributeType.Urgency) + { + state.UrgencyLevel = UrgencyLevel.None; + } + else if (state.AttributeType == AttributeType.Id) + { + state.Id = null; + } + else if (state.AttributeType == AttributeType.None) + { + } + else + { + throw new Exception($"Invalid AttributeType: {state.AttributeType}"); + } + + return await sc.ReplaceDialogAsync(Actions.UpdateAttribute); + } + protected async Task CheckDescription(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); @@ -383,7 +460,7 @@ protected TicketCard ConvertTicket(Ticket ticket) UrgencyLevel = $"{SharedStrings.Urgency}{ConvertUrgencyLevel(ticket.Urgency)}", State = $"{SharedStrings.TicketState}{ConvertTicketState(ticket.State)}", OpenedTime = $"{SharedStrings.OpenedAt}{ticket.OpenedTime.ToString()}", - Id = ticket.Id, + Id = $"{SharedStrings.ID}{ticket.Id}", ResolvedReason = ticket.ResolvedReason, Speak = ticket.Description }; @@ -394,7 +471,7 @@ protected KnowledgeCard ConvertKnowledge(Knowledge knowledge) { var card = new KnowledgeCard() { - Id = knowledge.Id, + Id = $"{SharedStrings.ID}{knowledge.Id}", Title = knowledge.Title, UpdatedTime = $"{SharedStrings.UpdatedAt}{knowledge.UpdatedTime.ToString()}", Content = knowledge.Content, @@ -442,13 +519,21 @@ protected string GetDivergedCardName(ITurnContext turnContext, string card) protected class Actions { - public const string CreateTicket = "CreateTicket"; public const string SetDescription = "SetDescription"; public const string SetUrgency = "SetUrgency"; + public const string SetId = "SetId"; + + public const string CreateTicket = "CreateTicket"; + public const string UpdateTicket = "UpdateTicket"; public const string UpdateAttribute = "UpdateAttribute"; public const string UpdateAttributeNoYesNo = "UpdateAttributeNoYesNo"; public const string UpdateAttributeHasYesNo = "UpdateAttributeHasYesNo"; + + public const string ShowTicket = "ShowTicket"; + public const string ShowAttribute = "ShowAttribute"; + public const string ShowAttributeNoYesNo = "ShowAttributeNoYesNo"; + public const string ShowAttributeHasYesNo = "ShowAttributeHasYesNo"; } } -} \ No newline at end of file +} diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs index d8e3d64253..3334776267 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs @@ -7,8 +7,9 @@ using System.Threading; using System.Threading.Tasks; using ITSMSkill.Models; -using ITSMSkill.Responses.Ticket; +using ITSMSkill.Prompts; using ITSMSkill.Responses.Shared; +using ITSMSkill.Responses.Ticket; using ITSMSkill.Services; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; @@ -50,11 +51,19 @@ public UpdateTicketDialog( AfterUpdateMore }; - AddDialog(new WaterfallDialog(Actions.UpdateTicket, updateTicket) { TelemetryClient = telemetryClient }); + var attributesForUpdate = new AttributeType[] { AttributeType.Description, AttributeType.Urgency }; + AddDialog(new WaterfallDialog(Actions.UpdateTicket, updateTicket) { TelemetryClient = telemetryClient }); AddDialog(new WaterfallDialog(Actions.UpdateAttribute, updateAttribute) { TelemetryClient = telemetryClient }); + AddDialog(new AttributePrompt(Actions.UpdateAttributeNoYesNo, attributesForUpdate, false)); + AddDialog(new AttributePrompt(Actions.UpdateAttributeHasYesNo, attributesForUpdate, true)); InitialDialogId = Actions.UpdateTicket; + + InputAttributeResponse = TicketResponses.UpdateAttribute; + InputAttributePrompt = Actions.UpdateAttributeNoYesNo; + InputMoreAttributeResponse = TicketResponses.UpdateAttributeMore; + InputMoreAttributePrompt = Actions.UpdateAttributeHasYesNo; } protected async Task UpdateAttributeLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) @@ -62,63 +71,6 @@ public UpdateTicketDialog( return await sc.BeginDialogAsync(Actions.UpdateAttribute); } - protected async Task UpdateSelectedAttribute(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) - { - var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - if (state.AttributeType == AttributeType.Description) - { - return await sc.BeginDialogAsync(Actions.SetDescription); - } - else if (state.AttributeType == AttributeType.Urgency) - { - return await sc.BeginDialogAsync(Actions.SetUrgency); - } - else - { - throw new Exception($"Invalid AttributeType: {state.AttributeType}"); - } - } - - protected async Task UpdateMore(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) - { - var options = new PromptOptions() - { - Prompt = ResponseManager.GetResponse(SharedResponses.InputAttributeMore) - }; - - return await sc.PromptAsync(Actions.UpdateAttributeHasYesNo, options); - } - - protected async Task AfterUpdateMore(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) - { - if (sc.Result == null) - { - return await sc.NextAsync(); - } - - var type = (AttributeType)sc.Result; - var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - state.AttributeType = type; - - if (state.AttributeType == AttributeType.Description) - { - state.TicketDescription = null; - } - else if (state.AttributeType == AttributeType.Urgency) - { - state.UrgencyLevel = UrgencyLevel.None; - } - else if (state.AttributeType == AttributeType.None) - { - } - else - { - throw new Exception($"Invalid AttributeType: {state.AttributeType}"); - } - - return await sc.ReplaceDialogAsync(Actions.UpdateAttribute); - } - protected async Task UpdateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs index 4eedc3b963..19648646cd 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs @@ -2,6 +2,8 @@ { public class CreateTicketRequest { + public string caller_id { get; set; } + public string short_description { get; set; } public string urgency { get; set; } diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/GetUserIdResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/GetUserIdResponse.cs new file mode 100644 index 0000000000..c499464b0e --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/GetUserIdResponse.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ITSMSkill.Models.ServiceNow +{ + public class GetUserIdResponse + { + public string result { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs index 5d53651cf6..4236209a50 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs @@ -34,6 +34,7 @@ public void DigestLuisResult(ITSMLuis luis) TicketDescription = string.Join(' ', luis.Entities.TicketDescription); } + // TODO only the first one is considered now if (luis.Entities.UrgencyLevel != null) { UrgencyLevel = Enum.Parse(luis.Entities.UrgencyLevel[0][0], true); diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs index 729c2db18a..26bef90c7f 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs @@ -24,8 +24,6 @@ public class SharedResponses : IResponseIdCollection public const string InputUrgency = "InputUrgency"; public const string ConfirmId = "ConfirmId"; public const string InputId = "InputId"; - public const string InputAttribute = "InputAttribute"; - public const string InputAttributeMore = "InputAttributeMore"; public const string IfExistingSolve = "IfExistingSolve"; public const string ExistingSolve = "ExistingSolve"; public const string ServiceFailed = "ServiceFailed"; diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json index 8f91268bbd..8ee1aa15a2 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json @@ -199,26 +199,6 @@ ], "inputHint": "expectingInput" }, - "InputAttribute": { - // same with SharedStrings - "suggestedActions": [ "description", "urgency" ], - "replies": [ - { - "text": "Please input the attribute to update:", - "speak": "Please input the attribute to update:" - } - ], - "inputHint": "expectingInput" - }, - "InputAttributeMore": { - "replies": [ - { - "text": "Do you want to update more attribute? Or input the attribute to update directly.", - "speak": "Do you want to update more attribute? Or input the attribute to update directly." - } - ], - "inputHint": "expectingInput" - }, "IfExistingSolve": { "replies": [ { diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs index 60adbec4df..ffa1e0b9dc 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs @@ -87,6 +87,24 @@ public static string AttributeUrgency { } } + /// + /// Looks up a localized string similar to Description: . + /// + public static string Description { + get { + return ResourceManager.GetString("Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ID: . + /// + public static string ID { + get { + return ResourceManager.GetString("ID", resourceCulture); + } + } + /// /// Looks up a localized string similar to Opened at . /// diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx index f6e4d96411..b03edf5853 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx @@ -165,4 +165,10 @@ Urgency: + + Description: + + + ID: + \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs index 90c6f4de48..774cda9b16 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs @@ -13,6 +13,14 @@ public class TicketResponses : IResponseIdCollection { // Generated accessors public const string TicketCreated = "TicketCreated"; + public const string UpdateAttribute = "UpdateAttribute"; + public const string UpdateAttributeMore = "UpdateAttributeMore"; public const string TicketUpdated = "TicketUpdated"; + public const string ShowConstraintNone = "ShowConstraintNone"; + public const string ShowConstraints = "ShowConstraints"; + public const string ShowAttribute = "ShowAttribute"; + public const string ShowAttributeMore = "ShowAttributeMore"; + public const string TicketShow = "TicketShow"; + public const string TicketShowNone = "TicketShowNone"; } } \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json index 7dd46db934..e362e75447 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json @@ -8,6 +8,26 @@ ], "inputHint": "acceptingInput" }, + "UpdateAttribute": { + // same with SharedStrings + "suggestedActions": [ "description", "urgency" ], + "replies": [ + { + "text": "Please input the attribute to update:", + "speak": "Please input the attribute to update:" + } + ], + "inputHint": "expectingInput" + }, + "UpdateAttributeMore": { + "replies": [ + { + "text": "Do you want to update more attributes? Or input the attribute type directly.", + "speak": "Do you want to update more attributes? Or input the attribute type directly." + } + ], + "inputHint": "expectingInput" + }, "TicketUpdated": { "replies": [ { @@ -16,5 +36,61 @@ } ], "inputHint": "acceptingInput" + }, + "ShowConstraintNone": { + "replies": [ + { + "text": "You don't have any constraints on search.", + "speak": "You don't have any constraints on search." + } + ], + "inputHint": "igoringInput" + }, + "ShowConstraints": { + "replies": [ + { + "text": "You have the following constraints on search:", + "speak": "You have the following constraints on search:" + } + ], + "inputHint": "igoringInput" + }, + "ShowAttribute": { + // same with SharedStrings + "suggestedActions": [ "id", "description", "urgency" ], + "replies": [ + { + "text": "Please input the attribute to constrain search:", + "speak": "Please input the attribute to constrain search:" + } + ], + "inputHint": "expectingInput" + }, + "ShowAttributeMore": { + "replies": [ + { + "text": "Do you want to add attributes to constrain? Or input the attribute type directly.", + "speak": "Do you want to add attributes to constrain? Or input the attribute type directly." + } + ], + "inputHint": "expectingInput" + }, + "TicketShow": { + "replies": [ + { + "text": "Here are your search results. Let me know when you need my help.", + "speak": "Here are your search results. Let me know when you need my help." + } + ], + "inputHint": "acceptingInput" + }, + "TicketShowNone": { + "replies": [ + { + "text": "I'm sorry I can't find any. Let me know when you need my help.", + "speak": "I'm sorry I can't find any. Let me know when you need my help." + } + ], + "inputHint": "acceptingInput" } } diff --git a/skills/src/csharp/experimental/itsmskill/Services/BotSettings.cs b/skills/src/csharp/experimental/itsmskill/Services/BotSettings.cs index 58b9d6c66d..2d437ad99e 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/BotSettings.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/BotSettings.cs @@ -9,6 +9,8 @@ public class BotSettings : BotSettingsBase { public string ServiceNowUrl { get; set; } + public string ServiceNowGetUserId { get; set; } + public int LimitSize { get; set; } } } \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs index 64f7c01450..baf5806133 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs @@ -17,7 +17,8 @@ public partial class ITSMLuis: IRecognizerConvert public enum Intent { None, TicketCreate, - TicketUpdate + TicketUpdate, + TicketShow }; public Dictionary Intents; diff --git a/skills/src/csharp/experimental/itsmskill/Services/ServiceManager.cs b/skills/src/csharp/experimental/itsmskill/Services/ServiceManager.cs index 7cc9c6b906..daa4f62dd4 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ServiceManager.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceManager.cs @@ -6,9 +6,9 @@ public class ServiceManager : IServiceManager { public IITServiceManagement CreateManagement(BotSettings botSettings, TokenResponse tokenResponse) { - if (!string.IsNullOrEmpty(botSettings.ServiceNowUrl) && tokenResponse.ConnectionName == "ServiceNow") + if (tokenResponse.ConnectionName == "ServiceNow" && !string.IsNullOrEmpty(botSettings.ServiceNowUrl) && !string.IsNullOrEmpty(botSettings.ServiceNowGetUserId)) { - return new ServiceNow.Management(botSettings.ServiceNowUrl, tokenResponse.Token, botSettings.LimitSize); + return new ServiceNow.Management(botSettings.ServiceNowUrl, tokenResponse.Token, botSettings.LimitSize, botSettings.ServiceNowGetUserId); } else { diff --git a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs index 7b59e40b39..3c0e3d8ffc 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs @@ -14,13 +14,14 @@ namespace ITSMSkill.Services.ServiceNow { public class Management : IITServiceManagement { - private static readonly string TicketResource = "table/incident"; - private static readonly string KnowledgeResource = "table/kb_knowledge"; + private static readonly string TicketResource = "now/v1/table/incident"; + private static readonly string KnowledgeResource = "now/v1/table/kb_knowledge"; private static readonly Dictionary UrgencyToString; private static readonly Dictionary StringToUrgency; private static readonly Dictionary TicketStateToString; private static readonly Dictionary StringToTicketState; private readonly RestClient client; + private readonly string GetUserIdResource; private readonly string token; private readonly int limitSize; @@ -47,34 +48,30 @@ static Management() StringToTicketState = new Dictionary(TicketStateToString.Select(pair => KeyValuePair.Create(pair.Value, pair.Key))); } - public Management(string url, string token, int limitSize) + public Management(string url, string token, int limitSize, string getUserIdResource) { - this.client = new RestClient($"{url}/api/now/v1/"); + this.client = new RestClient($"{url}/api/"); + this.GetUserIdResource = getUserIdResource; this.token = token; this.limitSize = limitSize; } public async Task CreateTicket(string description, UrgencyLevel urgency) { - var request = CreateRequest(TicketResource); - var body = new CreateTicketRequest() - { - short_description = description, - urgency = UrgencyToString[urgency] - }; - request.AddJsonBody(body); try { - var result = await client.PostAsync(request); + var request = CreateRequest(GetUserIdResource); + var userId = await client.GetAsync(request); - // didn't find way to get current user's id directly, so update again. or have to create a custom api like https://community.servicenow.com/community?id=community_question&sys_id=52efcb88db1ddb084816f3231f9619c7 - request = CreateRequest($"{TicketResource}/{result.result.sys_id}?sysparm_exclude_ref_link=true"); - var updateBody = new + request = CreateRequest(TicketResource); + var body = new CreateTicketRequest() { - caller_id = result.result.opened_by.value + caller_id = userId.result, + short_description = description, + urgency = UrgencyToString[urgency] }; - request.AddJsonBody(updateBody); - result = await client.PatchAsync(request); + request.AddJsonBody(body); + var result = await client.PostAsync(request); return new TicketsResult() { @@ -94,37 +91,42 @@ public async Task CreateTicket(string description, UrgencyLevel u public async Task SearchTicket(string description = null, List urgencies = null, string id = null, List states = null) { - var request = CreateRequest(TicketResource); - var sysparmQuery = new List(); - if (!string.IsNullOrEmpty(description)) + try { - sysparmQuery.Add($"short_descriptionLIKE{description}"); - } + var request = CreateRequest(GetUserIdResource); + var userId = await client.GetAsync(request); - if (urgencies != null && urgencies.Count > 0) - { - sysparmQuery.Add($"urgencyIN{string.Join(',', urgencies.Select(urgency => UrgencyToString[urgency]))}"); - } + request = CreateRequest(TicketResource); - if (!string.IsNullOrEmpty(id)) - { - sysparmQuery.Add($"sys_id={id}"); - } + var sysparmQuery = new List + { + $"caller_id={userId.result}" + }; - if (states != null && states.Count > 0) - { - sysparmQuery.Add($"stateIN{string.Join(',', states.Select(state => TicketStateToString[state]))}"); - } + if (!string.IsNullOrEmpty(description)) + { + sysparmQuery.Add($"short_descriptionLIKE{description}"); + } + + if (urgencies != null && urgencies.Count > 0) + { + sysparmQuery.Add($"urgencyIN{string.Join(',', urgencies.Select(urgency => UrgencyToString[urgency]))}"); + } + + if (!string.IsNullOrEmpty(id)) + { + sysparmQuery.Add($"sys_id={id}"); + } + + if (states != null && states.Count > 0) + { + sysparmQuery.Add($"stateIN{string.Join(',', states.Select(state => TicketStateToString[state]))}"); + } - if (sysparmQuery.Count > 0) - { request.AddParameter("sysparm_query", string.Join('^', sysparmQuery)); - } - request.AddParameter("sysparm_limit", limitSize); + request.AddParameter("sysparm_limit", limitSize); - try - { var result = await client.GetAsync(request); return new TicketsResult() { diff --git a/skills/src/csharp/experimental/itsmskill/Startup.cs b/skills/src/csharp/experimental/itsmskill/Startup.cs index db36d8154f..b281b14c52 100644 --- a/skills/src/csharp/experimental/itsmskill/Startup.cs +++ b/skills/src/csharp/experimental/itsmskill/Startup.cs @@ -105,6 +105,7 @@ public void ConfigureServices(IServiceCollection services) // Register dialogs services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); // Configure adapters diff --git a/skills/src/csharp/experimental/itsmskill/appsettings.json b/skills/src/csharp/experimental/itsmskill/appsettings.json index 488b388ed3..bd7ffb646b 100644 --- a/skills/src/csharp/experimental/itsmskill/appsettings.json +++ b/skills/src/csharp/experimental/itsmskill/appsettings.json @@ -21,5 +21,6 @@ "databaseId": "botstate-db" }, "serviceNowUrl": "https://instance.service-now.com", + "serviceNowGetUserId": "namespace/get_user_id", "limitSize": 5 } \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/readme.md b/skills/src/csharp/experimental/itsmskill/readme.md index f8ec0388c4..7a05ba96a9 100644 --- a/skills/src/csharp/experimental/itsmskill/readme.md +++ b/skills/src/csharp/experimental/itsmskill/readme.md @@ -3,6 +3,8 @@ To test this skill, one has to setup the following: * Create a ServiceNow instance in https://developer.servicenow.com/app.do#!/instance and update the serviceNowUrl of appsettings.json +* Set up a scripted REST API for current user's sys_id following https://community.servicenow.com/community?id=community_question&sys_id=52efcb88db1ddb084816f3231f9619c7 and update the serviceNowGetUserId of appsetting.json + - Please raise an issue if simpler way is found * Set up endpoint (https://docs.servicenow.com/bundle/london-platform-administration/page/administer/security/task/t_CreateEndpointforExternalClients.html#t_CreateEndpointforExternalClients) for Client id and Client secret in the following OAuth Connection - Redirect URL is https://token.botframework.com/.auth/web/redirect * Add an OAuth Connection in the Settings of Web App Bot named 'ServiceNow' with Service Provider 'Generic Oauth 2' From 1f0dafe29c5e9910ec716fc9daa11f371a661840 Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Tue, 13 Aug 2019 15:10:18 +0800 Subject: [PATCH 04/15] [ITSM] add close ticket action --- .../Deployment/Resources/LU/en/ITSM.lu | 3 + .../Resources/LU/en/Ticket/Close.lu | 17 ++++ .../Resources/LU/en/Ticket/Create.lu | 2 +- .../Resources/LU/en/Ticket/Update.lu | 8 +- .../itsmskill/Dialogs/CloseTicketDialog.cs | 84 +++++++++++++++++++ .../itsmskill/Dialogs/CreateTicketDialog.cs | 6 +- .../itsmskill/Dialogs/MainDialog.cs | 8 ++ .../itsmskill/Dialogs/SkillDialogBase.cs | 50 +++++++++++ .../itsmskill/Models/SkillState.cs | 8 ++ .../Responses/Shared/SharedResponses.cs | 2 + .../Responses/Shared/SharedResponses.json | 18 ++++ .../Responses/Ticket/TicketResponses.cs | 1 + .../Responses/Ticket/TicketResponses.json | 9 ++ .../Services/IITServiceManagement.cs | 2 + .../itsmskill/Services/ITSMLuis.cs | 5 +- .../Services/ServiceNow/Management.cs | 37 ++++++++ .../csharp/experimental/itsmskill/Startup.cs | 1 + 17 files changed, 252 insertions(+), 9 deletions(-) create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Close.lu create mode 100644 skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu index 8ec4754740..1b4c1a9fed 100644 --- a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu @@ -1,6 +1,7 @@ [Ticket Create](./Ticket/Create.lu) [Ticket Update](./Ticket/Update.lu) [Ticket Show](./Ticket/Show.lu) +[Ticket Close](./Ticket/Close.lu) # None > from chitchat @@ -21,6 +22,8 @@ $TicketDescription:simple +$CloseReason:simple + > # List entities $AttributeType:urgency= diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Close.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Close.lu new file mode 100644 index 0000000000..7b37620f4d --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Close.lu @@ -0,0 +1,17 @@ +# TicketClose + +- close a ticket +- close an issue +- close an incident +- close a ticket because {CloseReason=Received help from agent} +- close an issue due to {CloseReason=Wrong setup} +- resolve a ticket +- resolve an issue +- resolve an incident +- resolve a ticket because {CloseReason=Gave workaround} +- resolve an issue due to {CloseReason=Fixed} +- i would like to resolve a ticket +- i would like to resolve an issue +- i would like to resolve an incident +- i would like to close a ticket because {CloseReason=Reinstalled the app} +- i would like to close an issue due to {CloseReason=Reverted to a previous version} diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Create.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Create.lu index 038bd1a951..47c3f35137 100644 --- a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Create.lu +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Create.lu @@ -14,7 +14,7 @@ - create an incident for {TicketDescription=disk is still having issues} - raise a ticket - raise an incident -- raise a issue +- raise an issue - i would like to add a ticket - i would like to open a ticket about {TicketDescription=lost connection} - i would like to create a ticket for {TicketDescription=server down again} diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Update.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Update.lu index 4aeaa9e526..eaeee1a6d6 100644 --- a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Update.lu +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Update.lu @@ -2,17 +2,17 @@ - update a ticket - update an incident -- update a issue +- update an issue - update ticket's urgency to high - update ticket's description tp {TicketDescription=can't log} - change a ticket - change an incident -- change a issue +- change an issue - change ticket's urgency to low - change ticket's description to {TicketDescription=unable to access} - i would like to change a ticket - i would like to change an incident -- i would like to change a issue +- i would like to change an issue - i would like to update a ticket - i would like to update an incident -- i would like to update a issue +- i would like to update an issue diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs new file mode 100644 index 0000000000..bfb128d21b --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Specialized; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ITSMSkill.Models; +using ITSMSkill.Prompts; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Responses.Ticket; +using ITSMSkill.Services; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Dialogs.Choices; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Connector; +using Microsoft.Bot.Schema; + +namespace ITSMSkill.Dialogs +{ + public class CloseTicketDialog : SkillDialogBase + { + public CloseTicketDialog( + BotSettings settings, + BotServices services, + ResponseManager responseManager, + ConversationState conversationState, + IServiceManager serviceManager, + IBotTelemetryClient telemetryClient) + : base(nameof(CloseTicketDialog), settings, services, responseManager, conversationState, serviceManager, telemetryClient) + { + var closeTicket = new WaterfallStep[] + { + CheckId, + InputId, + SetId, + CheckReason, + InputReason, + SetReason, + GetAuthToken, + AfterGetAuthToken, + CloseTicket + }; + + AddDialog(new WaterfallDialog(Actions.CloseTicket, closeTicket)); + + InitialDialogId = Actions.CloseTicket; + } + + protected async Task CloseTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (state.Token == null) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); + return await sc.EndDialogAsync(); + } + + var management = ServiceManager.CreateManagement(Settings, state.Token); + var result = await management.CloseTicket(id: state.Id, reason: state.CloseReason); + + if (!result.Success) + { + var errorReplacements = new StringDictionary + { + { "Error", result.ErrorMessage } + }; + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); + return await sc.EndDialogAsync(); + } + + var card = new Card() + { + Name = GetDivergedCardName(sc.Context, "Ticket"), + Data = ConvertTicket(result.Tickets[0]) + }; + + await sc.Context.SendActivityAsync(ResponseManager.GetCardResponse(TicketResponses.TicketClosed, card, null)); + return await sc.NextAsync(); + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs index 2692edaf74..54968ba2eb 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs @@ -51,7 +51,7 @@ public CreateTicketDialog( InitialDialogId = Actions.CreateTicket; } - public async Task DisplayExisting(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + protected async Task DisplayExisting(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); if (state.Token == null) @@ -105,7 +105,7 @@ public CreateTicketDialog( } } - public async Task IfExistingSolve(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + protected async Task IfExistingSolve(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { if ((bool)sc.Result) { @@ -118,7 +118,7 @@ public CreateTicketDialog( } } - public async Task CreateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + protected async Task CreateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); if (state.Token == null) diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs index e205855e66..e86eb7ab9a 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs @@ -41,6 +41,7 @@ public MainDialog( CreateTicketDialog createTicketDialog, UpdateTicketDialog updateTicketDialog, ShowTicketDialog showTicketDialog, + CloseTicketDialog closeTicketDialog, IBotTelemetryClient telemetryClient) : base(nameof(MainDialog), telemetryClient) { @@ -57,6 +58,7 @@ public MainDialog( AddDialog(createTicketDialog); AddDialog(updateTicketDialog); AddDialog(showTicketDialog); + AddDialog(closeTicketDialog); } protected override async Task OnStartAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) @@ -113,6 +115,12 @@ public MainDialog( break; } + case ITSMLuis.Intent.TicketClose: + { + turnResult = await dc.BeginDialogAsync(nameof(CloseTicketDialog)); + break; + } + case ITSMLuis.Intent.None: { // No intent was identified, send confused message diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs index bff4a0bc16..639f26865e 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs @@ -340,6 +340,54 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can return await sc.NextAsync(); } + protected async Task CheckReason(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (string.IsNullOrEmpty(state.CloseReason)) + { + return await sc.NextAsync(false); + } + else + { + var replacements = new StringDictionary + { + { "Reason", state.CloseReason } + }; + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.ConfirmReason, replacements) + }; + + return await sc.PromptAsync(nameof(ConfirmPrompt), options); + } + } + + protected async Task InputReason(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (!(bool)sc.Result || string.IsNullOrEmpty(state.CloseReason)) + { + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.InputReason) + }; + + return await sc.PromptAsync(nameof(TextPrompt), options); + } + else + { + return await sc.NextAsync(state.CloseReason); + } + } + + protected async Task SetReason(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.CloseReason = (string)sc.Result; + return await sc.NextAsync(); + } + protected async Task CheckUrgency(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); @@ -534,6 +582,8 @@ protected class Actions public const string ShowAttribute = "ShowAttribute"; public const string ShowAttributeNoYesNo = "ShowAttributeNoYesNo"; public const string ShowAttributeHasYesNo = "ShowAttributeHasYesNo"; + + public const string CloseTicket = "CloseTicket"; } } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs index 4236209a50..be7eb3b854 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs @@ -21,6 +21,8 @@ public SkillState() public string TicketDescription { get; set; } + public string CloseReason { get; set; } + public UrgencyLevel UrgencyLevel { get; set; } public AttributeType AttributeType { get; set; } @@ -34,6 +36,11 @@ public void DigestLuisResult(ITSMLuis luis) TicketDescription = string.Join(' ', luis.Entities.TicketDescription); } + if (luis.Entities.CloseReason != null) + { + CloseReason = string.Join(' ', luis.Entities.CloseReason); + } + // TODO only the first one is considered now if (luis.Entities.UrgencyLevel != null) { @@ -51,6 +58,7 @@ public void Clear() Token = null; Id = null; TicketDescription = null; + CloseReason = null; UrgencyLevel = UrgencyLevel.None; AttributeType = AttributeType.None; } diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs index 26bef90c7f..1125878338 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs @@ -20,6 +20,8 @@ public class SharedResponses : IResponseIdCollection public const string ErrorMessage = "ErrorMessage"; public const string ConfirmDescription = "ConfirmDescription"; public const string InputDescription = "InputDescription"; + public const string ConfirmReason = "ConfirmReason"; + public const string InputReason = "InputReason"; public const string ConfirmUrgency = "ConfirmUrgency"; public const string InputUrgency = "InputUrgency"; public const string ConfirmId = "ConfirmId"; diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json index 8ee1aa15a2..d19f566caa 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json @@ -163,6 +163,24 @@ ], "inputHint": "expectingInput" }, + "ConfirmReason": { + "replies": [ + { + "text": "Is the reason \"{Reason}\" correct?", + "speak": "Is the reason \"{Reason}\" correct?" + } + ], + "inputHint": "expectingInput" + }, + "InputReason": { + "replies": [ + { + "text": "Please input the reason:", + "speak": "Please input the reason:" + } + ], + "inputHint": "expectingInput" + }, "ConfirmUrgency": { "replies": [ { diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs index 774cda9b16..f3425a516c 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs @@ -22,5 +22,6 @@ public class TicketResponses : IResponseIdCollection public const string ShowAttributeMore = "ShowAttributeMore"; public const string TicketShow = "TicketShow"; public const string TicketShowNone = "TicketShowNone"; + public const string TicketClosed = "TicketClosed"; } } \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json index e362e75447..8b5456c6dc 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json @@ -92,5 +92,14 @@ } ], "inputHint": "acceptingInput" + }, + "TicketClosed": { + "replies": [ + { + "text": "Your ticket has been closed. Let me know when you need my help.", + "speak": "Your ticket has been closed. Let me know when you need my help." + } + ], + "inputHint": "acceptingInput" } } diff --git a/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs b/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs index bb86aae52f..c612995180 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs @@ -13,6 +13,8 @@ public interface IITServiceManagement Task UpdateTicket(string id, string description = null, UrgencyLevel urgency = UrgencyLevel.None); + Task CloseTicket(string id, string reason); + Task SearchKnowledge(string query); } } diff --git a/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs index baf5806133..8427785759 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs @@ -18,7 +18,8 @@ public enum Intent { None, TicketCreate, TicketUpdate, - TicketShow + TicketShow, + TicketClose }; public Dictionary Intents; @@ -26,6 +27,7 @@ public class _Entities { // Simple entities public string[] TicketDescription; + public string[] CloseReason; // Lists public string[][] AttributeType; @@ -35,6 +37,7 @@ public class _Entities public class _Instance { public InstanceData[] TicketDescription; + public InstanceData[] CloseReason; public InstanceData[] AttributeType; public InstanceData[] UrgencyLevel; } diff --git a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs index 3c0e3d8ffc..c44dbcacd5 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs @@ -174,6 +174,43 @@ public async Task UpdateTicket(string id, string description = nu } } + public async Task CloseTicket(string id, string reason) + { + try + { + // minimum field required: https://community.servicenow.com/community?id=community_question&sys_id=84ceb6a5db58dbc01dcaf3231f9619e9 + var request = CreateRequest(GetUserIdResource); + var userId = await client.GetAsync(request); + + request = CreateRequest($"{TicketResource}/{id}?sysparm_exclude_ref_link=true"); + var body = new + { + close_code = "Closed/Resolved by Caller", + state = "7", + caller_id = userId.result, + close_notes = reason + }; + request.JsonSerializer = new JsonNoNull(); + request.AddJsonBody(body); + + var result = await client.PatchAsync(request); + + return new TicketsResult() + { + Success = true, + Tickets = new Ticket[] { ConvertTicket(result.result) } + }; + } + catch (Exception ex) + { + return new TicketsResult() + { + Success = false, + ErrorMessage = ex.Message + }; + } + } + public async Task SearchKnowledge(string query) { var request = CreateRequest(KnowledgeResource); diff --git a/skills/src/csharp/experimental/itsmskill/Startup.cs b/skills/src/csharp/experimental/itsmskill/Startup.cs index b281b14c52..59dab2f356 100644 --- a/skills/src/csharp/experimental/itsmskill/Startup.cs +++ b/skills/src/csharp/experimental/itsmskill/Startup.cs @@ -106,6 +106,7 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); // Configure adapters From 21b3715934d351ea502a74601e29aced5f62e69a Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Tue, 13 Aug 2019 17:19:07 +0800 Subject: [PATCH 05/15] [ITSM] add show knowledge action --- .../itsmskill/Adapters/CustomSkillAdapter.cs | 4 +- .../itsmskill/Adapters/DefaultAdapter.cs | 4 +- .../Deployment/Resources/LU/en/ITSM.lu | 1 + .../Resources/LU/en/Knowledge/Show.lu | 17 ++++ .../itsmskill/Dialogs/MainDialog.cs | 11 ++- .../itsmskill/Dialogs/ShowKnowledgeDialog.cs | 94 +++++++++++++++++++ .../itsmskill/Dialogs/ShowTicketDialog.cs | 5 - .../itsmskill/Dialogs/SkillDialogBase.cs | 2 + .../experimental/itsmskill/ITSMSkill.csproj | 26 ++++- .../Responses/Knowledge/KnowledgeResponses.cs | 18 ++++ .../Knowledge/KnowledgeResponses.json | 20 ++++ .../Responses/Knowledge/KnowledgeResponses.tt | 3 + .../itsmskill/Services/ITSMLuis.cs | 3 +- .../Services/ServiceNow/Management.cs | 10 +- .../csharp/experimental/itsmskill/Startup.cs | 5 +- 15 files changed, 202 insertions(+), 21 deletions(-) create mode 100644 skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Knowledge/Show.lu create mode 100644 skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.json create mode 100644 skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.tt diff --git a/skills/src/csharp/experimental/itsmskill/Adapters/CustomSkillAdapter.cs b/skills/src/csharp/experimental/itsmskill/Adapters/CustomSkillAdapter.cs index 41470fdfcb..5f2a524393 100644 --- a/skills/src/csharp/experimental/itsmskill/Adapters/CustomSkillAdapter.cs +++ b/skills/src/csharp/experimental/itsmskill/Adapters/CustomSkillAdapter.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System.Globalization; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Services; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Azure; using Microsoft.Bot.Builder.Dialogs; @@ -9,8 +11,6 @@ using Microsoft.Bot.Builder.Solutions.Middleware; using Microsoft.Bot.Builder.Solutions.Responses; using Microsoft.Bot.Schema; -using ITSMSkill.Responses.Shared; -using ITSMSkill.Services; namespace ITSMSkill.Adapters { diff --git a/skills/src/csharp/experimental/itsmskill/Adapters/DefaultAdapter.cs b/skills/src/csharp/experimental/itsmskill/Adapters/DefaultAdapter.cs index 55dad2d848..d821ed779e 100644 --- a/skills/src/csharp/experimental/itsmskill/Adapters/DefaultAdapter.cs +++ b/skills/src/csharp/experimental/itsmskill/Adapters/DefaultAdapter.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System.Globalization; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Services; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Azure; using Microsoft.Bot.Builder.Integration.AspNet.Core; @@ -9,8 +11,6 @@ using Microsoft.Bot.Builder.Solutions.Responses; using Microsoft.Bot.Connector.Authentication; using Microsoft.Bot.Schema; -using ITSMSkill.Responses.Shared; -using ITSMSkill.Services; namespace ITSMSkill.Bots { diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu index 1b4c1a9fed..f80e551037 100644 --- a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu @@ -2,6 +2,7 @@ [Ticket Update](./Ticket/Update.lu) [Ticket Show](./Ticket/Show.lu) [Ticket Close](./Ticket/Close.lu) +[Knowledge Show](./Knowledge/Show.lu) # None > from chitchat diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Knowledge/Show.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Knowledge/Show.lu new file mode 100644 index 0000000000..5524c8a405 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Knowledge/Show.lu @@ -0,0 +1,17 @@ +# KnowledgeShow + +- show knowledgebase +- show knowledge articles +- show knowledge database +- show knowledge articles related to {TicketDescription=lost connection} +- show knowledge database about {TicketDescription=new employee hire} +- search knowledgebase +- search knowledge articles +- search knowledge database +- search knowledge articles related to {TicketDescription=unable to connect} +- search knowledge database about {TicketDescription=disk is still having issues} +- explore knowledgebase +- explore knowledge articles +- explore knowledge database +- explore knowledge articles related to {TicketDescription=need oracle installed} +- explore knowledge database about {TicketDescription=unable to connect} diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs index e86eb7ab9a..9b0bc18c2c 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs @@ -42,6 +42,7 @@ public MainDialog( UpdateTicketDialog updateTicketDialog, ShowTicketDialog showTicketDialog, CloseTicketDialog closeTicketDialog, + ShowKnowledgeDialog showKnowledgeDialog, IBotTelemetryClient telemetryClient) : base(nameof(MainDialog), telemetryClient) { @@ -59,11 +60,11 @@ public MainDialog( AddDialog(updateTicketDialog); AddDialog(showTicketDialog); AddDialog(closeTicketDialog); + AddDialog(showKnowledgeDialog); } protected override async Task OnStartAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) { - var locale = CultureInfo.CurrentUICulture; await dc.Context.SendActivityAsync(_responseManager.GetResponse(MainResponses.WelcomeMessage)); } @@ -121,6 +122,12 @@ public MainDialog( break; } + case ITSMLuis.Intent.KnowledgeShow: + { + turnResult = await dc.BeginDialogAsync(nameof(ShowKnowledgeDialog)); + break; + } + case ITSMLuis.Intent.None: { // No intent was identified, send confused message @@ -148,7 +155,7 @@ public MainDialog( protected override async Task CompleteAsync(DialogContext dc, DialogTurnResult result = null, CancellationToken cancellationToken = default(CancellationToken)) { // workaround. if connect skill directly to teams, the following response does not work. - if (dc.Context.Adapter is IRemoteUserTokenProvider remoteInvocationAdapter || Channel.GetChannelId(dc.Context) != Channels.Msteams) + if (dc.Context.Adapter is IRemoteUserTokenProvider || Channel.GetChannelId(dc.Context) != Channels.Msteams) { var response = dc.Context.Activity.CreateReply(); response.Type = ActivityTypes.EndOfConversation; diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs new file mode 100644 index 0000000000..1ed488addc --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ITSMSkill.Models; +using ITSMSkill.Prompts; +using ITSMSkill.Responses.Knowledge; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Services; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Dialogs.Choices; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Connector; +using Microsoft.Bot.Schema; + +namespace ITSMSkill.Dialogs +{ + public class ShowKnowledgeDialog : SkillDialogBase + { + public ShowKnowledgeDialog( + BotSettings settings, + BotServices services, + ResponseManager responseManager, + ConversationState conversationState, + IServiceManager serviceManager, + IBotTelemetryClient telemetryClient) + : base(nameof(ShowKnowledgeDialog), settings, services, responseManager, conversationState, serviceManager, telemetryClient) + { + var showKnowledge = new WaterfallStep[] + { + CheckDescription, + InputDescription, + SetDescription, + GetAuthToken, + AfterGetAuthToken, + ShowKnowledge + }; + + AddDialog(new WaterfallDialog(Actions.ShowKnowledge, showKnowledge)); + + InitialDialogId = Actions.ShowKnowledge; + } + + protected async Task ShowKnowledge(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (state.Token == null) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); + return await sc.EndDialogAsync(); + } + + var management = ServiceManager.CreateManagement(Settings, state.Token); + var result = await management.SearchKnowledge(state.TicketDescription); + + if (!result.Success) + { + var errorReplacements = new StringDictionary + { + { "Error", result.ErrorMessage } + }; + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); + return await sc.EndDialogAsync(); + } + + if (result.Knowledges == null || result.Knowledges.Length == 0) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(KnowledgeResponses.KnowledgeShowNone)); + return await sc.NextAsync(); + } + else + { + var cards = new List(); + foreach (var knowledge in result.Knowledges) + { + cards.Add(new Card() + { + Name = GetDivergedCardName(sc.Context, "Knowledge"), + Data = ConvertKnowledge(knowledge) + }); + } + + await sc.Context.SendActivityAsync(ResponseManager.GetCardResponse(KnowledgeResponses.KnowledgeShow, cards)); + return await sc.NextAsync(); + } + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs index f7a5c23b63..a0dbd2001a 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs @@ -148,11 +148,6 @@ public ShowTicketDialog( }); } - var options = new PromptOptions() - { - Prompt = ResponseManager.GetCardResponse(TicketResponses.TicketShow, cards) - }; - await sc.Context.SendActivityAsync(ResponseManager.GetCardResponse(TicketResponses.TicketShow, cards)); return await sc.NextAsync(); } diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs index 639f26865e..8e8114bb70 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs @@ -584,6 +584,8 @@ protected class Actions public const string ShowAttributeHasYesNo = "ShowAttributeHasYesNo"; public const string CloseTicket = "CloseTicket"; + + public const string ShowKnowledge = "ShowKnowledge"; } } } diff --git a/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj index df61d3473e..a7fe5e5114 100644 --- a/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj +++ b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj @@ -10,18 +10,29 @@ + + + + + + - + + + + TextTemplatingFileGenerator + TicketResponses.cs + @@ -60,7 +71,11 @@ - + + TextTemplatingFileGenerator + KnowledgeResponses.cs + + TextTemplatingFileGenerator TicketResponses.cs @@ -87,7 +102,12 @@ - + + True + True + KnowledgeResponses.tt + + True True TicketResponses.tt diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs new file mode 100644 index 0000000000..38e8d4a920 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs @@ -0,0 +1,18 @@ +// https://docs.microsoft.com/en-us/visualstudio/modeling/t4-include-directive?view=vs-2017 +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder.Solutions.Responses; + +namespace ITSMSkill.Responses.Knowledge +{ + /// + /// Contains bot responses. + /// + public class KnowledgeResponses : IResponseIdCollection + { + // Generated accessors + public const string KnowledgeShow = "KnowledgeShow"; + public const string KnowledgeShowNone = "KnowledgeShowNone"; + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.json new file mode 100644 index 0000000000..bd77cb4361 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.json @@ -0,0 +1,20 @@ +{ + "KnowledgeShow": { + "replies": [ + { + "text": "Here are your search results. Let me know when you need my help.", + "speak": "Here are your search results. Let me know when you need my help." + } + ], + "inputHint": "acceptingInput" + }, + "KnowledgeShowNone": { + "replies": [ + { + "text": "I'm sorry I can't find any. Let me know when you need my help.", + "speak": "I'm sorry I can't find any. Let me know when you need my help." + } + ], + "inputHint": "acceptingInput" + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.tt b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.tt new file mode 100644 index 0000000000..f204f0981b --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.tt @@ -0,0 +1,3 @@ +<#@ template debug="false" hostspecific="true" language="C#" #> +<#@ output extension=".cs" #> +<#@ include file="..\Shared\ResponseIdCollection.t4"#> \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs index 8427785759..6231461c1b 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs @@ -19,7 +19,8 @@ public enum Intent { TicketCreate, TicketUpdate, TicketShow, - TicketClose + TicketClose, + KnowledgeShow }; public Dictionary Intents; diff --git a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs index c44dbcacd5..d4c82a5011 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs @@ -21,7 +21,7 @@ public class Management : IITServiceManagement private static readonly Dictionary TicketStateToString; private static readonly Dictionary StringToTicketState; private readonly RestClient client; - private readonly string GetUserIdResource; + private readonly string getUserIdResource; private readonly string token; private readonly int limitSize; @@ -51,7 +51,7 @@ static Management() public Management(string url, string token, int limitSize, string getUserIdResource) { this.client = new RestClient($"{url}/api/"); - this.GetUserIdResource = getUserIdResource; + this.getUserIdResource = getUserIdResource; this.token = token; this.limitSize = limitSize; } @@ -60,7 +60,7 @@ public async Task CreateTicket(string description, UrgencyLevel u { try { - var request = CreateRequest(GetUserIdResource); + var request = CreateRequest(getUserIdResource); var userId = await client.GetAsync(request); request = CreateRequest(TicketResource); @@ -93,7 +93,7 @@ public async Task SearchTicket(string description = null, List(request); request = CreateRequest(TicketResource); @@ -179,7 +179,7 @@ public async Task CloseTicket(string id, string reason) try { // minimum field required: https://community.servicenow.com/community?id=community_question&sys_id=84ceb6a5db58dbc01dcaf3231f9619e9 - var request = CreateRequest(GetUserIdResource); + var request = CreateRequest(getUserIdResource); var userId = await client.GetAsync(request); request = CreateRequest($"{TicketResource}/{id}?sysparm_exclude_ref_link=true"); diff --git a/skills/src/csharp/experimental/itsmskill/Startup.cs b/skills/src/csharp/experimental/itsmskill/Startup.cs index 59dab2f356..bce7a9120d 100644 --- a/skills/src/csharp/experimental/itsmskill/Startup.cs +++ b/skills/src/csharp/experimental/itsmskill/Startup.cs @@ -5,9 +5,10 @@ using ITSMSkill.Adapters; using ITSMSkill.Bots; using ITSMSkill.Dialogs; -using ITSMSkill.Responses.Ticket; +using ITSMSkill.Responses.Knowledge; using ITSMSkill.Responses.Main; using ITSMSkill.Responses.Shared; +using ITSMSkill.Responses.Ticket; using ITSMSkill.Services; using Microsoft.ApplicationInsights; using Microsoft.AspNetCore.Builder; @@ -97,6 +98,7 @@ public void ConfigureServices(IServiceCollection services) settings.CognitiveModels.Select(l => l.Key).ToArray(), new MainResponses(), new TicketResponses(), + new KnowledgeResponses(), new SharedResponses())); // Configure service @@ -107,6 +109,7 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); // Configure adapters From b32e4dd919d8122c12bbfc97b820737b22ad0583 Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Tue, 13 Aug 2019 20:19:29 +0800 Subject: [PATCH 06/15] [ITSM] update update ticket action redesign AttributePrompt & enum to string --- .../Resources/LU/en/Ticket/Update.lu | 2 +- .../itsmskill/Dialogs/ShowTicketDialog.cs | 29 ++-- .../itsmskill/Dialogs/SkillDialogBase.cs | 127 ++++++------------ .../itsmskill/Dialogs/UpdateTicketDialog.cs | 54 ++++++-- .../experimental/itsmskill/ITSMSkill.csproj | 5 + .../itsmskill/Models/AttributeType.cs | 9 +- .../itsmskill/Models/SkillState.cs | 14 ++ .../itsmskill/Models/TicketState.cs | 12 +- .../itsmskill/Models/UrgencyLevel.cs | 9 +- ...butePrompt.cs => AttributeWithNoPrompt.cs} | 30 ++--- .../Responses/Ticket/TicketResponses.cs | 5 +- .../Responses/Ticket/TicketResponses.json | 43 +++--- .../Utilities/EnumToLocalizedString.cs | 50 +++++++ 13 files changed, 233 insertions(+), 156 deletions(-) rename skills/src/csharp/experimental/itsmskill/Prompts/{AttributePrompt.cs => AttributeWithNoPrompt.cs} (71%) create mode 100644 skills/src/csharp/experimental/itsmskill/Utilities/EnumToLocalizedString.cs diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Update.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Update.lu index eaeee1a6d6..a77b7ad20d 100644 --- a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Update.lu +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Update.lu @@ -4,7 +4,7 @@ - update an incident - update an issue - update ticket's urgency to high -- update ticket's description tp {TicketDescription=can't log} +- update ticket's description to {TicketDescription=can't log} - change a ticket - change an incident - change an issue diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs index a0dbd2001a..a7b1bc68af 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs @@ -13,6 +13,7 @@ using ITSMSkill.Responses.Shared; using ITSMSkill.Responses.Ticket; using ITSMSkill.Services; +using ITSMSkill.Utilities; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder.Dialogs.Choices; @@ -44,26 +45,25 @@ public ShowTicketDialog( var showAttribute = new WaterfallStep[] { ShowConstraints, - UpdateMore, - AfterUpdateMore, - CheckAttributeNoConfirm, + CheckAttribute, + InputAttribute, SetAttribute, - UpdateSelectedAttribute + UpdateSelectedAttribute, + ShowLoop }; var attributesForShow = new AttributeType[] { AttributeType.Id, AttributeType.Description, AttributeType.Urgency }; AddDialog(new WaterfallDialog(Actions.ShowTicket, showTicket) { TelemetryClient = telemetryClient }); AddDialog(new WaterfallDialog(Actions.ShowAttribute, showAttribute) { TelemetryClient = telemetryClient }); - AddDialog(new AttributePrompt(Actions.ShowAttributeNoYesNo, attributesForShow, false)); - AddDialog(new AttributePrompt(Actions.ShowAttributeHasYesNo, attributesForShow, true)); + AddDialog(new AttributeWithNoPrompt(Actions.ShowAttributePrompt, attributesForShow)); InitialDialogId = Actions.ShowTicket; + // never used + // ConfirmAttributeResponse InputAttributeResponse = TicketResponses.ShowAttribute; - InputAttributePrompt = Actions.ShowAttributeNoYesNo; - InputMoreAttributeResponse = TicketResponses.ShowAttributeMore; - InputMoreAttributePrompt = Actions.ShowAttributeHasYesNo; + InputAttributePrompt = Actions.ShowAttributePrompt; } protected async Task ShowAttributeLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) @@ -74,6 +74,10 @@ public ShowTicketDialog( protected async Task ShowConstraints(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + + // always prompt for search + state.AttributeType = AttributeType.None; + var sb = new StringBuilder(); if (!string.IsNullOrEmpty(state.Id)) { @@ -87,7 +91,7 @@ public ShowTicketDialog( if (state.UrgencyLevel != UrgencyLevel.None) { - sb.AppendLine($"{SharedStrings.Urgency}{ConvertUrgencyLevel(state.UrgencyLevel)}"); + sb.AppendLine($"{SharedStrings.Urgency}{state.UrgencyLevel.ToLocalizedString()}"); } if (sb.Length == 0) @@ -103,6 +107,11 @@ public ShowTicketDialog( return await sc.NextAsync(); } + protected async Task ShowLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + return await sc.ReplaceDialogAsync(Actions.ShowAttribute); + } + protected async Task ShowTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs index 8e8114bb70..41bf2326d7 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs @@ -12,6 +12,7 @@ using ITSMSkill.Prompts; using ITSMSkill.Responses.Shared; using ITSMSkill.Services; +using ITSMSkill.Utilities; using Luis; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; @@ -92,14 +93,12 @@ public SkillDialogBase( protected IServiceManager ServiceManager { get; set; } + protected string ConfirmAttributeResponse { get; set; } + protected string InputAttributeResponse { get; set; } protected string InputAttributePrompt { get; set; } - protected string InputMoreAttributeResponse { get; set; } - - protected string InputMoreAttributePrompt { get; set; } - protected override async Task OnBeginDialogAsync(DialogContext dc, object options, CancellationToken cancellationToken = default(CancellationToken)) { return await base.OnBeginDialogAsync(dc, options, cancellationToken); @@ -202,10 +201,33 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can return await sc.NextAsync(); } - protected async Task CheckAttributeNoConfirm(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + protected async Task CheckAttribute(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); if (state.AttributeType == AttributeType.None) + { + return await sc.NextAsync(false); + } + else + { + var replacements = new StringDictionary + { + { "Attribute", state.AttributeType.ToLocalizedString() } + }; + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(ConfirmAttributeResponse, replacements) + }; + + return await sc.PromptAsync(nameof(ConfirmPrompt), options); + } + } + + protected async Task InputAttribute(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (!(bool)sc.Result || state.AttributeType == AttributeType.None) { var options = new PromptOptions() { @@ -222,6 +244,11 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can protected async Task SetAttribute(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { + if (sc.Result == null) + { + return await sc.EndDialogAsync(); + } + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); state.AttributeType = (AttributeType)sc.Result; return await sc.NextAsync(); @@ -248,50 +275,6 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can } } - protected async Task UpdateMore(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) - { - var options = new PromptOptions() - { - Prompt = ResponseManager.GetResponse(InputMoreAttributeResponse) - }; - - return await sc.PromptAsync(InputMoreAttributePrompt, options); - } - - protected async Task AfterUpdateMore(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) - { - if (sc.Result == null) - { - return await sc.EndDialogAsync(); - } - - var type = (AttributeType)sc.Result; - var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - state.AttributeType = type; - - if (state.AttributeType == AttributeType.Description) - { - state.TicketDescription = null; - } - else if (state.AttributeType == AttributeType.Urgency) - { - state.UrgencyLevel = UrgencyLevel.None; - } - else if (state.AttributeType == AttributeType.Id) - { - state.Id = null; - } - else if (state.AttributeType == AttributeType.None) - { - } - else - { - throw new Exception($"Invalid AttributeType: {state.AttributeType}"); - } - - return await sc.ReplaceDialogAsync(Actions.UpdateAttribute); - } - protected async Task CheckDescription(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); @@ -423,15 +406,15 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can { new Choice() { - Value = UrgencyLevel.Low.ToString() + Value = UrgencyLevel.Low.ToLocalizedString() }, new Choice() { - Value = UrgencyLevel.Medium.ToString() + Value = UrgencyLevel.Medium.ToLocalizedString() }, new Choice() { - Value = UrgencyLevel.High.ToString() + Value = UrgencyLevel.High.ToLocalizedString() } } }; @@ -440,9 +423,10 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can } else { + // use Index to skip localization return await sc.NextAsync(new FoundChoice() { - Value = state.UrgencyLevel.ToString() + Index = (int)state.UrgencyLevel - 1 }); } } @@ -450,7 +434,7 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can protected async Task SetUrgency(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - state.UrgencyLevel = Enum.Parse(((FoundChoice)sc.Result).Value); + state.UrgencyLevel = (UrgencyLevel)(((FoundChoice)sc.Result).Index + 1); return await sc.NextAsync(); } @@ -505,8 +489,8 @@ protected TicketCard ConvertTicket(Ticket ticket) var card = new TicketCard() { Description = ticket.Description, - UrgencyLevel = $"{SharedStrings.Urgency}{ConvertUrgencyLevel(ticket.Urgency)}", - State = $"{SharedStrings.TicketState}{ConvertTicketState(ticket.State)}", + UrgencyLevel = $"{SharedStrings.Urgency}{ticket.Urgency.ToLocalizedString()}", + State = $"{SharedStrings.TicketState}{ticket.State.ToLocalizedString()}", OpenedTime = $"{SharedStrings.OpenedAt}{ticket.OpenedTime.ToString()}", Id = $"{SharedStrings.ID}{ticket.Id}", ResolvedReason = ticket.ResolvedReason, @@ -528,31 +512,6 @@ protected KnowledgeCard ConvertKnowledge(Knowledge knowledge) return card; } - protected string ConvertUrgencyLevel(UrgencyLevel urgency) - { - switch (urgency) - { - case UrgencyLevel.Low: return SharedStrings.UrgencyLow; - case UrgencyLevel.Medium: return SharedStrings.UrgencyMedium; - case UrgencyLevel.High: return SharedStrings.UrgencyHigh; - default: return string.Empty; - } - } - - protected string ConvertTicketState(TicketState state) - { - switch (state) - { - case TicketState.New: return SharedStrings.TicketStateNew; - case TicketState.InProgress: return SharedStrings.TicketStateInProgress; - case TicketState.OnHold: return SharedStrings.TicketStateOnHold; - case TicketState.Resolved: return SharedStrings.TicketStateResolved; - case TicketState.Closed: return SharedStrings.TicketStateClosed; - case TicketState.Canceled: return SharedStrings.TicketStateCanceled; - default: return string.Empty; - } - } - protected string GetDivergedCardName(ITurnContext turnContext, string card) { if (Channel.GetChannelId(turnContext) == Channels.Msteams) @@ -575,13 +534,11 @@ protected class Actions public const string UpdateTicket = "UpdateTicket"; public const string UpdateAttribute = "UpdateAttribute"; - public const string UpdateAttributeNoYesNo = "UpdateAttributeNoYesNo"; - public const string UpdateAttributeHasYesNo = "UpdateAttributeHasYesNo"; + public const string UpdateAttributePrompt = "UpdateAttributePrompt"; public const string ShowTicket = "ShowTicket"; public const string ShowAttribute = "ShowAttribute"; - public const string ShowAttributeNoYesNo = "ShowAttributeNoYesNo"; - public const string ShowAttributeHasYesNo = "ShowAttributeHasYesNo"; + public const string ShowAttributePrompt = "ShowAttributePrompt"; public const string CloseTicket = "CloseTicket"; diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs index 3334776267..daa8cbb26e 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs @@ -4,13 +4,16 @@ using System; using System.Collections.Specialized; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using ITSMSkill.Models; +using ITSMSkill.Models.ServiceNow; using ITSMSkill.Prompts; using ITSMSkill.Responses.Shared; using ITSMSkill.Responses.Ticket; using ITSMSkill.Services; +using ITSMSkill.Utilities; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder.Dialogs.Choices; @@ -44,26 +47,25 @@ public UpdateTicketDialog( var updateAttribute = new WaterfallStep[] { - CheckAttributeNoConfirm, + ShowUpdates, + CheckAttribute, + InputAttribute, SetAttribute, UpdateSelectedAttribute, - UpdateMore, - AfterUpdateMore + UpdateLoop }; var attributesForUpdate = new AttributeType[] { AttributeType.Description, AttributeType.Urgency }; AddDialog(new WaterfallDialog(Actions.UpdateTicket, updateTicket) { TelemetryClient = telemetryClient }); AddDialog(new WaterfallDialog(Actions.UpdateAttribute, updateAttribute) { TelemetryClient = telemetryClient }); - AddDialog(new AttributePrompt(Actions.UpdateAttributeNoYesNo, attributesForUpdate, false)); - AddDialog(new AttributePrompt(Actions.UpdateAttributeHasYesNo, attributesForUpdate, true)); + AddDialog(new AttributeWithNoPrompt(Actions.UpdateAttributePrompt, attributesForUpdate)); InitialDialogId = Actions.UpdateTicket; + ConfirmAttributeResponse = TicketResponses.ConfirmUpdateAttribute; InputAttributeResponse = TicketResponses.UpdateAttribute; - InputAttributePrompt = Actions.UpdateAttributeNoYesNo; - InputMoreAttributeResponse = TicketResponses.UpdateAttributeMore; - InputMoreAttributePrompt = Actions.UpdateAttributeHasYesNo; + InputAttributePrompt = Actions.UpdateAttributePrompt; } protected async Task UpdateAttributeLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) @@ -71,6 +73,42 @@ public UpdateTicketDialog( return await sc.BeginDialogAsync(Actions.UpdateAttribute); } + protected async Task ShowUpdates(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + var sb = new StringBuilder(); + + if (!string.IsNullOrEmpty(state.TicketDescription)) + { + sb.AppendLine($"{SharedStrings.Description}{state.TicketDescription}"); + } + + if (state.UrgencyLevel != UrgencyLevel.None) + { + sb.AppendLine($"{SharedStrings.Urgency}{state.UrgencyLevel.ToLocalizedString()}"); + } + + if (sb.Length == 0) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.ShowUpdateNone)); + } + else + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.ShowUpdates)); + await sc.Context.SendActivityAsync(sb.ToString()); + } + + return await sc.NextAsync(); + } + + protected async Task UpdateLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.AttributeType = AttributeType.None; + + return await sc.ReplaceDialogAsync(Actions.UpdateAttribute); + } + protected async Task UpdateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); diff --git a/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj index a7fe5e5114..c59de510dc 100644 --- a/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj +++ b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj @@ -132,6 +132,11 @@ True SharedStrings.resx + + True + True + TicketResponses.tt + diff --git a/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs b/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs index 4a540226c2..b9ae102648 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs @@ -1,15 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Utilities; namespace ITSMSkill.Models { public enum AttributeType { None, + [EnumLocalizedDescription("AttributeId", typeof(SharedStrings))] Id, + [EnumLocalizedDescription("AttributeDescription", typeof(SharedStrings))] Description, + [EnumLocalizedDescription("AttributeUrgency", typeof(SharedStrings))] Urgency } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs index be7eb3b854..a9e69b7f0d 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs @@ -51,6 +51,20 @@ public void DigestLuisResult(ITSMLuis luis) { AttributeType = Enum.Parse(luis.Entities.AttributeType[0][0], true); } + + var topIntent = luis.TopIntent().intent; + if (topIntent == ITSMLuis.Intent.TicketUpdate) + { + // clear AttributeType if already set + if (AttributeType == AttributeType.Description && !string.IsNullOrEmpty(TicketDescription)) + { + AttributeType = AttributeType.None; + } + else if (AttributeType == AttributeType.Urgency && UrgencyLevel != UrgencyLevel.None) + { + AttributeType = AttributeType.None; + } + } } public void Clear() diff --git a/skills/src/csharp/experimental/itsmskill/Models/TicketState.cs b/skills/src/csharp/experimental/itsmskill/Models/TicketState.cs index 09611abc7a..b47e6e8ee1 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/TicketState.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/TicketState.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Utilities; namespace ITSMSkill.Models { @@ -9,11 +7,17 @@ namespace ITSMSkill.Models public enum TicketState { None, + [EnumLocalizedDescription("TicketStateNew", typeof(SharedStrings))] New, + [EnumLocalizedDescription("TicketStateInProgress", typeof(SharedStrings))] InProgress, + [EnumLocalizedDescription("TicketStateOnHold", typeof(SharedStrings))] OnHold, + [EnumLocalizedDescription("TicketStateResolved", typeof(SharedStrings))] Resolved, + [EnumLocalizedDescription("TicketStateClosed", typeof(SharedStrings))] Closed, + [EnumLocalizedDescription("TicketStateCanceled", typeof(SharedStrings))] Canceled } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/UrgencyLevel.cs b/skills/src/csharp/experimental/itsmskill/Models/UrgencyLevel.cs index a0bce98d30..3c053d178f 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/UrgencyLevel.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/UrgencyLevel.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using ITSMSkill.Responses.Shared; +using ITSMSkill.Utilities; namespace ITSMSkill.Models { @@ -9,8 +7,11 @@ namespace ITSMSkill.Models public enum UrgencyLevel { None, + [EnumLocalizedDescription("UrgencyLow", typeof(SharedStrings))] Low, + [EnumLocalizedDescription("UrgencyMedium", typeof(SharedStrings))] Medium, + [EnumLocalizedDescription("UrgencyHigh", typeof(SharedStrings))] High } } diff --git a/skills/src/csharp/experimental/itsmskill/Prompts/AttributePrompt.cs b/skills/src/csharp/experimental/itsmskill/Prompts/AttributeWithNoPrompt.cs similarity index 71% rename from skills/src/csharp/experimental/itsmskill/Prompts/AttributePrompt.cs rename to skills/src/csharp/experimental/itsmskill/Prompts/AttributeWithNoPrompt.cs index cfa7db1528..afc3bc5a46 100644 --- a/skills/src/csharp/experimental/itsmskill/Prompts/AttributePrompt.cs +++ b/skills/src/csharp/experimental/itsmskill/Prompts/AttributeWithNoPrompt.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using ITSMSkill.Models; using ITSMSkill.Responses.Shared; +using ITSMSkill.Utilities; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder.Solutions.Util; @@ -11,15 +12,13 @@ namespace ITSMSkill.Prompts { - public class AttributePrompt : Prompt + public class AttributeWithNoPrompt : Prompt { - private readonly bool hasYesNo; private readonly AttributeType[] attributes; - public AttributePrompt(string dialogId, AttributeType[] attributes, bool hasYesNo, PromptValidator validator = null, string defaultLocale = null) + public AttributeWithNoPrompt(string dialogId, AttributeType[] attributes, PromptValidator validator = null, string defaultLocale = null) : base(dialogId, validator) { - this.hasYesNo = hasYesNo; this.attributes = attributes; DefaultLocale = defaultLocale; } @@ -60,21 +59,11 @@ public AttributePrompt(string dialogId, AttributeType[] attributes, bool hasYesN { var message = turnContext.Activity.AsMessageActivity(); - if (hasYesNo) + var promptRecognizerResult = ConfirmRecognizerHelper.ConfirmYesOrNo(message.Text, turnContext.Activity.Locale); + if (promptRecognizerResult.Succeeded && !promptRecognizerResult.Value) { - var promptRecognizerResult = ConfirmRecognizerHelper.ConfirmYesOrNo(message.Text, turnContext.Activity.Locale); - if (promptRecognizerResult.Succeeded) - { - result.Succeeded = true; - if (promptRecognizerResult.Value) - { - result.Value = AttributeType.None; - } - else - { - result.Value = null; - } - } + result.Succeeded = true; + result.Value = null; } if (!result.Succeeded) @@ -99,9 +88,8 @@ private bool IsMessageAttributeMatch(string message, AttributeType attribute) { switch (attribute) { - case AttributeType.Description: return message.Equals(SharedStrings.AttributeDescription); - case AttributeType.Urgency: return message.Equals(SharedStrings.AttributeUrgency); - default: return false; + case AttributeType.None: return false; + default: return message.Equals(attribute.ToLocalizedString()); } } } diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs index f3425a516c..87aa41ba36 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs @@ -13,13 +13,14 @@ public class TicketResponses : IResponseIdCollection { // Generated accessors public const string TicketCreated = "TicketCreated"; + public const string ConfirmUpdateAttribute = "ConfirmUpdateAttribute"; public const string UpdateAttribute = "UpdateAttribute"; - public const string UpdateAttributeMore = "UpdateAttributeMore"; public const string TicketUpdated = "TicketUpdated"; public const string ShowConstraintNone = "ShowConstraintNone"; public const string ShowConstraints = "ShowConstraints"; + public const string ShowUpdateNone = "ShowUpdateNone"; + public const string ShowUpdates = "ShowUpdates"; public const string ShowAttribute = "ShowAttribute"; - public const string ShowAttributeMore = "ShowAttributeMore"; public const string TicketShow = "TicketShow"; public const string TicketShowNone = "TicketShowNone"; public const string TicketClosed = "TicketClosed"; diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json index 8b5456c6dc..406b512b38 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json @@ -8,22 +8,22 @@ ], "inputHint": "acceptingInput" }, - "UpdateAttribute": { - // same with SharedStrings - "suggestedActions": [ "description", "urgency" ], + "ConfirmUpdateAttribute": { "replies": [ { - "text": "Please input the attribute to update:", - "speak": "Please input the attribute to update:" + "text": "Do you want to update {Attribute}?", + "speak": "Do you want to update {Attribute}?" } ], "inputHint": "expectingInput" }, - "UpdateAttributeMore": { + "UpdateAttribute": { + // same with SharedStrings + "suggestedActions": [ "description", "urgency", "no" ], "replies": [ { - "text": "Do you want to update more attributes? Or input the attribute type directly.", - "speak": "Do you want to update more attributes? Or input the attribute type directly." + "text": "What attribute do you want to set for updating? Or \"No\" for no more attribute.", + "speak": "What attribute do you want to set for updating? Or \"No\" for no more attribute." } ], "inputHint": "expectingInput" @@ -55,22 +55,31 @@ ], "inputHint": "igoringInput" }, - "ShowAttribute": { - // same with SharedStrings - "suggestedActions": [ "id", "description", "urgency" ], + "ShowUpdateNone": { "replies": [ { - "text": "Please input the attribute to constrain search:", - "speak": "Please input the attribute to constrain search:" + "text": "You don't have any to update.", + "speak": "You don't have any to update." } ], - "inputHint": "expectingInput" + "inputHint": "igoringInput" }, - "ShowAttributeMore": { + "ShowUpdates": { + "replies": [ + { + "text": "You have the following to update:", + "speak": "You have the following to update:" + } + ], + "inputHint": "igoringInput" + }, + "ShowAttribute": { + // same with SharedStrings + "suggestedActions": [ "id", "description", "urgency", "no" ], "replies": [ { - "text": "Do you want to add attributes to constrain? Or input the attribute type directly.", - "speak": "Do you want to add attributes to constrain? Or input the attribute type directly." + "text": "What attribute do you want to set as search constraint? Or \"No\" for no more attribute.", + "speak": "What attribute do you want to set as search constraint? Or \"No\" for no more attribute." } ], "inputHint": "expectingInput" diff --git a/skills/src/csharp/experimental/itsmskill/Utilities/EnumToLocalizedString.cs b/skills/src/csharp/experimental/itsmskill/Utilities/EnumToLocalizedString.cs new file mode 100644 index 0000000000..a972cdd51f --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Utilities/EnumToLocalizedString.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Resources; +using System.Threading.Tasks; + +// https://stackoverflow.com/questions/17380900/enum-localization +namespace ITSMSkill.Utilities +{ + public static class EnumToLocalizedString + { + public static string ToLocalizedString(this Enum enumValue) + { + FieldInfo fi = enumValue.GetType().GetField(enumValue.ToString()); + + DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false); + + if (attributes != null && attributes.Length > 0) + { + return attributes[0].Description; + } + else + { + return enumValue.ToString(); + } + } + } + + public class EnumLocalizedDescription : DescriptionAttribute + { + private readonly string key; + private readonly ResourceManager resource; + + public EnumLocalizedDescription(string key, Type type) + { + this.resource = new ResourceManager(type); + this.key = key; + } + + public override string Description + { + get + { + return resource.GetString(key); + } + } + } +} From 6db44188cd5c98f2fcf637b71bcdc4cca14fb8e7 Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Wed, 14 Aug 2019 18:11:17 +0800 Subject: [PATCH 07/15] [ITSM] add next/prev * add class GeneralPrompt for next/prev navigation * add next/prev to show knowledge base and ticket * show knowledge invoke create ticket if required * replace wrong EndDialogAsync to CancelAllDialogsAsync --- .../LU/en/{general.lu => General.lu} | 0 .../itsmskill/Dialogs/CloseTicketDialog.cs | 4 +- .../itsmskill/Dialogs/CreateTicketDialog.cs | 91 +++------- .../itsmskill/Dialogs/MainDialog.cs | 8 + .../itsmskill/Dialogs/ShowKnowledgeDialog.cs | 72 ++++---- .../itsmskill/Dialogs/ShowTicketDialog.cs | 111 ++++++++++-- .../itsmskill/Dialogs/SkillDialogBase.cs | 161 +++++++++++++++++- .../itsmskill/Dialogs/UpdateTicketDialog.cs | 13 +- .../itsmskill/Models/SkillState.cs | 21 ++- .../Prompts/AttributeWithNoPrompt.cs | 5 +- .../itsmskill/Prompts/GeneralPrompt.cs | 72 ++++++++ .../Responses/Knowledge/KnowledgeResponses.cs | 3 + .../Knowledge/KnowledgeResponses.json | 24 ++- .../Responses/Shared/SharedResponses.cs | 2 - .../Responses/Shared/SharedResponses.json | 18 -- .../Responses/Ticket/TicketResponses.cs | 2 + .../Responses/Ticket/TicketResponses.json | 30 +++- .../Services/IITServiceManagement.cs | 4 +- .../Services/ServiceNow/Management.cs | 8 +- 19 files changed, 496 insertions(+), 153 deletions(-) rename skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/{general.lu => General.lu} (100%) create mode 100644 skills/src/csharp/experimental/itsmskill/Prompts/GeneralPrompt.cs diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/general.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/General.lu similarity index 100% rename from skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/general.lu rename to skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/General.lu diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs index bfb128d21b..65df37e9f9 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs @@ -55,7 +55,7 @@ public CloseTicketDialog( if (state.Token == null) { await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); - return await sc.EndDialogAsync(); + return await sc.CancelAllDialogsAsync(); } var management = ServiceManager.CreateManagement(Settings, state.Token); @@ -68,7 +68,7 @@ public CloseTicketDialog( { "Error", result.ErrorMessage } }; await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); - return await sc.EndDialogAsync(); + return await sc.CancelAllDialogsAsync(); } var card = new Card() diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs index 54968ba2eb..e09962498a 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs @@ -8,9 +8,12 @@ using System.Threading; using System.Threading.Tasks; using ITSMSkill.Models; +using ITSMSkill.Prompts; +using ITSMSkill.Responses.Knowledge; using ITSMSkill.Responses.Shared; using ITSMSkill.Responses.Ticket; using ITSMSkill.Services; +using Luis; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder.Dialogs.Choices; @@ -35,10 +38,7 @@ public CreateTicketDialog( CheckDescription, InputDescription, SetDescription, - GetAuthToken, - AfterGetAuthToken, - DisplayExisting, - IfExistingSolve, + DisplayExistingLoop, CheckUrgency, InputUrgency, SetUrgency, @@ -46,75 +46,40 @@ public CreateTicketDialog( AfterGetAuthToken, CreateTicket }; - AddDialog(new WaterfallDialog(Actions.CreateTicket, createTicket)); - - InitialDialogId = Actions.CreateTicket; - } - protected async Task DisplayExisting(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) - { - var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - if (state.Token == null) + var displayExisting = new WaterfallStep[] { - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); - return await sc.EndDialogAsync(); - } - - var management = ServiceManager.CreateManagement(Settings, state.Token); - var result = await management.SearchKnowledge(state.TicketDescription); - - if (!result.Success) - { - var errorReplacements = new StringDictionary - { - { "Error", result.ErrorMessage } - }; - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); - return await sc.EndDialogAsync(); - } - - if (result.Knowledges == null || result.Knowledges.Length == 0) - { - return await sc.NextAsync(false); - } - else - { - var cards = new List(); - foreach (var knowledge in result.Knowledges) - { - cards.Add(new Card() - { - Name = GetDivergedCardName(sc.Context, "Knowledge"), - Data = ConvertKnowledge(knowledge) - }); - } + GetAuthToken, + AfterGetAuthToken, + ShowKnowledge, + IfKnowledgeHelp + }; - var options = new PromptOptions() - { - Prompt = ResponseManager.GetCardResponse(SharedResponses.IfExistingSolve, cards) - }; + AddDialog(new WaterfallDialog(Actions.CreateTicket, createTicket)); + AddDialog(new WaterfallDialog(Actions.DisplayExisting, displayExisting)); - // Workaround. In teams, HeroCard will be used for prompt and adaptive card could not be shown. So send them separatly - if (Channel.GetChannelId(sc.Context) == Channels.Msteams) - { - await sc.Context.SendActivityAsync(options.Prompt); - options.Prompt = null; - } + InitialDialogId = Actions.CreateTicket; - return await sc.PromptAsync(nameof(ConfirmPrompt), options); - } + // intended null + // ShowKnowledgeNoResponse + ShowKnowledgeEndResponse = KnowledgeResponses.KnowledgeEnd; + ShowKnowledgeResponse = TicketResponses.IfExistingSolve; + ShowKnowledgePrompt = Actions.NavigateYesNoPrompt; + KnowledgeHelpLoop = Actions.DisplayExisting; } - protected async Task IfExistingSolve(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + protected async Task DisplayExistingLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { - if ((bool)sc.Result) + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + + if (state.SkipDisplayExisting) { - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ExistingSolve)); - return await sc.EndDialogAsync(); + return await sc.NextAsync(); } else { - return await sc.NextAsync(); + state.PageIndex = -1; + return await sc.BeginDialogAsync(Actions.DisplayExisting); } } @@ -124,7 +89,7 @@ public CreateTicketDialog( if (state.Token == null) { await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); - return await sc.EndDialogAsync(); + return await sc.CancelAllDialogsAsync(); } var management = ServiceManager.CreateManagement(Settings, state.Token); @@ -137,7 +102,7 @@ public CreateTicketDialog( { "Error", result.ErrorMessage } }; await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); - return await sc.EndDialogAsync(); + return await sc.CancelAllDialogsAsync(); } var card = new Card() diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs index 9b0bc18c2c..f69a9fdeca 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs @@ -208,10 +208,14 @@ public MainDialog( else { var luisResult = await luisService.RecognizeAsync(dc.Context, cancellationToken); + + var state = await _stateAccessor.GetAsync(dc.Context, () => new SkillState()); + var topIntent = luisResult.TopIntent(); if (topIntent.score > 0.5) { + state.GeneralIntent = topIntent.intent; switch (topIntent.intent) { case GeneralLuis.Intent.Cancel: @@ -233,6 +237,10 @@ public MainDialog( } } } + else + { + state.GeneralIntent = GeneralLuis.Intent.None; + } } } diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs index 1ed488addc..03a3dda87f 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs @@ -12,6 +12,7 @@ using ITSMSkill.Responses.Knowledge; using ITSMSkill.Responses.Shared; using ITSMSkill.Services; +using Luis; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder.Dialogs.Choices; @@ -39,55 +40,62 @@ public ShowKnowledgeDialog( SetDescription, GetAuthToken, AfterGetAuthToken, - ShowKnowledge + ShowKnowledgeLoop, + IfCreateTicket, + AfterIfCreateTicket + }; + + var showKnowledgeLoop = new WaterfallStep[] + { + GetAuthToken, + AfterGetAuthToken, + ShowKnowledge, + IfKnowledgeHelp }; AddDialog(new WaterfallDialog(Actions.ShowKnowledge, showKnowledge)); + AddDialog(new WaterfallDialog(Actions.ShowKnowledgeLoop, showKnowledgeLoop)); InitialDialogId = Actions.ShowKnowledge; + + ShowKnowledgeNoResponse = KnowledgeResponses.KnowledgeShowNone; + ShowKnowledgeEndResponse = KnowledgeResponses.KnowledgeEnd; + ShowKnowledgeResponse = KnowledgeResponses.IfFindWanted; + ShowKnowledgePrompt = Actions.NavigateYesNoPrompt; + KnowledgeHelpLoop = Actions.ShowKnowledgeLoop; } - protected async Task ShowKnowledge(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + protected async Task ShowKnowledgeLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - if (state.Token == null) - { - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); - return await sc.EndDialogAsync(); - } + state.PageIndex = -1; - var management = ServiceManager.CreateManagement(Settings, state.Token); - var result = await management.SearchKnowledge(state.TicketDescription); + return await sc.BeginDialogAsync(Actions.ShowKnowledgeLoop); + } - if (!result.Success) + protected async Task IfCreateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var options = new PromptOptions() { - var errorReplacements = new StringDictionary - { - { "Error", result.ErrorMessage } - }; - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); - return await sc.EndDialogAsync(); - } + Prompt = ResponseManager.GetResponse(KnowledgeResponses.IfCreateTicket) + }; + + return await sc.PromptAsync(nameof(ConfirmPrompt), options); + } - if (result.Knowledges == null || result.Knowledges.Length == 0) + protected async Task AfterIfCreateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + if ((bool)sc.Result) { - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(KnowledgeResponses.KnowledgeShowNone)); - return await sc.NextAsync(); + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.SkipDisplayExisting = true; + + return await sc.ReplaceDialogAsync(nameof(CreateTicketDialog)); } else { - var cards = new List(); - foreach (var knowledge in result.Knowledges) - { - cards.Add(new Card() - { - Name = GetDivergedCardName(sc.Context, "Knowledge"), - Data = ConvertKnowledge(knowledge) - }); - } - - await sc.Context.SendActivityAsync(ResponseManager.GetCardResponse(KnowledgeResponses.KnowledgeShow, cards)); - return await sc.NextAsync(); + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ActionEnded)); + return await sc.CancelAllDialogsAsync(); } } } diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs index a7b1bc68af..e2471a86d8 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs @@ -9,11 +9,13 @@ using System.Threading; using System.Threading.Tasks; using ITSMSkill.Models; +using ITSMSkill.Models.ServiceNow; using ITSMSkill.Prompts; using ITSMSkill.Responses.Shared; using ITSMSkill.Responses.Ticket; using ITSMSkill.Services; using ITSMSkill.Utilities; +using Luis; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder.Dialogs.Choices; @@ -37,9 +39,7 @@ public ShowTicketDialog( var showTicket = new WaterfallStep[] { ShowAttributeLoop, - GetAuthToken, - AfterGetAuthToken, - ShowTicket + ShowTicketLoop }; var showAttribute = new WaterfallStep[] @@ -52,11 +52,20 @@ public ShowTicketDialog( ShowLoop }; + var showTicketLoop = new WaterfallStep[] + { + GetAuthToken, + AfterGetAuthToken, + ShowTicket, + IfContinueShow + }; + var attributesForShow = new AttributeType[] { AttributeType.Id, AttributeType.Description, AttributeType.Urgency }; AddDialog(new WaterfallDialog(Actions.ShowTicket, showTicket) { TelemetryClient = telemetryClient }); AddDialog(new WaterfallDialog(Actions.ShowAttribute, showAttribute) { TelemetryClient = telemetryClient }); AddDialog(new AttributeWithNoPrompt(Actions.ShowAttributePrompt, attributesForShow)); + AddDialog(new WaterfallDialog(Actions.ShowTicketLoop, showTicketLoop) { TelemetryClient = telemetryClient }); InitialDialogId = Actions.ShowTicket; @@ -71,6 +80,14 @@ public ShowTicketDialog( return await sc.BeginDialogAsync(Actions.ShowAttribute); } + protected async Task ShowTicketLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.PageIndex = -1; + + return await sc.BeginDialogAsync(Actions.ShowTicketLoop); + } + protected async Task ShowConstraints(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); @@ -100,8 +117,12 @@ public ShowTicketDialog( } else { - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.ShowConstraints)); - await sc.Context.SendActivityAsync(sb.ToString()); + var token = new StringDictionary() + { + { "Attributes", sb.ToString() } + }; + + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.ShowConstraints, token)); } return await sc.NextAsync(); @@ -118,7 +139,14 @@ public ShowTicketDialog( if (state.Token == null) { await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); - return await sc.EndDialogAsync(); + return await sc.CancelAllDialogsAsync(); + } + + bool firstDisplay = false; + if (state.PageIndex == -1) + { + firstDisplay = true; + state.PageIndex = 0; } var management = ServiceManager.CreateManagement(Settings, state.Token); @@ -128,7 +156,7 @@ public ShowTicketDialog( urgencies.Add(state.UrgencyLevel); } - var result = await management.SearchTicket(description: state.TicketDescription, urgencies: urgencies, id: state.Id); + var result = await management.SearchTicket(state.PageIndex, description: state.TicketDescription, urgencies: urgencies, id: state.Id); if (!result.Success) { @@ -137,13 +165,30 @@ public ShowTicketDialog( { "Error", result.ErrorMessage } }; await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); - return await sc.EndDialogAsync(); + return await sc.CancelAllDialogsAsync(); } if (result.Tickets == null || result.Tickets.Length == 0) { - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.TicketShowNone)); - return await sc.NextAsync(); + if (firstDisplay) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.TicketShowNone)); + return await sc.EndDialogAsync(); + } + else + { + var token = new StringDictionary() + { + { "Page", (state.PageIndex + 1).ToString() } + }; + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(TicketResponses.TicketEnd, token) + }; + + return await sc.PromptAsync(Actions.NavigateNoPrompt, options); + } } else { @@ -157,8 +202,50 @@ public ShowTicketDialog( }); } - await sc.Context.SendActivityAsync(ResponseManager.GetCardResponse(TicketResponses.TicketShow, cards)); - return await sc.NextAsync(); + var token = new StringDictionary() + { + { "Page", (state.PageIndex + 1).ToString() } + }; + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetCardResponse(TicketResponses.TicketShow, cards, token) + }; + + // Workaround. In teams, HeroCard will be used for prompt and adaptive card could not be shown. So send them separatly + if (Channel.GetChannelId(sc.Context) == Channels.Msteams) + { + await sc.Context.SendActivityAsync(options.Prompt); + options.Prompt = null; + } + + return await sc.PromptAsync(Actions.NavigateNoPrompt, options); + } + } + + protected async Task IfContinueShow(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var intent = (GeneralLuis.Intent)sc.Result; + if (intent == GeneralLuis.Intent.Reject) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ActionEnded)); + return await sc.CancelAllDialogsAsync(); + } + else if (intent == GeneralLuis.Intent.ShowNext) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.PageIndex += 1; + return await sc.ReplaceDialogAsync(Actions.ShowTicketLoop); + } + else if (intent == GeneralLuis.Intent.ShowPrevious) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.PageIndex = Math.Max(0, state.PageIndex - 1); + return await sc.ReplaceDialogAsync(Actions.ShowTicketLoop); + } + else + { + throw new Exception($"Invalid GeneralLuis.Intent ${intent}"); } } } diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs index 41bf2326d7..59e7290848 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs @@ -6,6 +6,7 @@ using System.Collections.Specialized; using System.Globalization; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using ITSMSkill.Models; @@ -75,12 +76,29 @@ public SkillDialogBase( SetId }; + var navigateYesNo = new HashSet() + { + GeneralLuis.Intent.ShowNext, + GeneralLuis.Intent.ShowPrevious, + GeneralLuis.Intent.Confirm, + GeneralLuis.Intent.Reject + }; + + var navigateNo = new HashSet() + { + GeneralLuis.Intent.ShowNext, + GeneralLuis.Intent.ShowPrevious, + GeneralLuis.Intent.Reject + }; + AddDialog(new TextPrompt(nameof(TextPrompt))); AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt))); AddDialog(new ChoicePrompt(nameof(ChoicePrompt))); AddDialog(new WaterfallDialog(Actions.SetDescription, setDescription)); AddDialog(new WaterfallDialog(Actions.SetUrgency, setUrgency)); AddDialog(new WaterfallDialog(Actions.SetId, setId)); + AddDialog(new GeneralPrompt(Actions.NavigateYesNoPrompt, navigateYesNo, StateAccessor)); + AddDialog(new GeneralPrompt(Actions.NavigateNoPrompt, navigateNo, StateAccessor)); } protected BotSettings Settings { get; set; } @@ -99,6 +117,16 @@ public SkillDialogBase( protected string InputAttributePrompt { get; set; } + protected string ShowKnowledgeNoResponse { get; set; } + + protected string ShowKnowledgeEndResponse { get; set; } + + protected string ShowKnowledgeResponse { get; set; } + + protected string ShowKnowledgePrompt { get; set; } + + protected string KnowledgeHelpLoop { get; set; } + protected override async Task OnBeginDialogAsync(DialogContext dc, object options, CancellationToken cancellationToken = default(CancellationToken)) { return await base.OnBeginDialogAsync(dc, options, cancellationToken); @@ -131,13 +159,18 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can { try { + var state = await StateAccessor.GetAsync(sc.Context); + // When the user authenticates interactively we pass on the tokens/Response event which surfaces as a JObject // When the token is cached we get a TokenResponse object. if (sc.Result is ProviderTokenResponse providerTokenResponse) { - var state = await StateAccessor.GetAsync(sc.Context); state.Token = providerTokenResponse.TokenResponse; } + else + { + state.Token = null; + } return await sc.NextAsync(); } @@ -438,6 +471,124 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can return await sc.NextAsync(); } + protected async Task ShowKnowledge(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (state.Token == null) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); + return await sc.CancelAllDialogsAsync(); + } + + bool firstDisplay = false; + if (state.PageIndex == -1) + { + firstDisplay = true; + state.PageIndex = 0; + } + + var management = ServiceManager.CreateManagement(Settings, state.Token); + var result = await management.SearchKnowledge(state.TicketDescription, state.PageIndex); + + if (!result.Success) + { + var errorReplacements = new StringDictionary + { + { "Error", result.ErrorMessage } + }; + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); + return await sc.CancelAllDialogsAsync(); + } + + if (result.Knowledges == null || result.Knowledges.Length == 0) + { + if (firstDisplay) + { + if (!string.IsNullOrEmpty(ShowKnowledgeNoResponse)) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(ShowKnowledgeNoResponse)); + } + + return await sc.EndDialogAsync(); + } + else + { + var token = new StringDictionary() + { + { "Page", (state.PageIndex + 1).ToString() } + }; + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(ShowKnowledgeEndResponse, token) + }; + + return await sc.PromptAsync(ShowKnowledgePrompt, options); + } + } + else + { + var cards = new List(); + foreach (var knowledge in result.Knowledges) + { + cards.Add(new Card() + { + Name = GetDivergedCardName(sc.Context, "Knowledge"), + Data = ConvertKnowledge(knowledge) + }); + } + + var token = new StringDictionary() + { + { "Page", (state.PageIndex + 1).ToString() } + }; + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetCardResponse(ShowKnowledgeResponse, cards, token) + }; + + // Workaround. In teams, HeroCard will be used for prompt and adaptive card could not be shown. So send them separatly + if (Channel.GetChannelId(sc.Context) == Channels.Msteams) + { + await sc.Context.SendActivityAsync(options.Prompt); + options.Prompt = null; + } + + return await sc.PromptAsync(ShowKnowledgePrompt, options); + } + } + + protected async Task IfKnowledgeHelp(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var intent = (GeneralLuis.Intent)sc.Result; + if (intent == GeneralLuis.Intent.Confirm) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ActionEnded)); + return await sc.CancelAllDialogsAsync(); + } + else if (intent == GeneralLuis.Intent.Reject) + { + return await sc.EndDialogAsync(); + } + else if (intent == GeneralLuis.Intent.ShowNext) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.PageIndex += 1; + return await sc.ReplaceDialogAsync(KnowledgeHelpLoop); + } + else if (intent == GeneralLuis.Intent.ShowPrevious) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.PageIndex = Math.Max(0, state.PageIndex - 1); + return await sc.ReplaceDialogAsync(KnowledgeHelpLoop); + } + else + { + throw new Exception($"Invalid GeneralLuis.Intent ${intent}"); + } + } + // Validators protected Task TokenResponseValidator(PromptValidatorContext pc, CancellationToken cancellationToken) { @@ -481,7 +632,7 @@ protected async Task HandleDialogExceptions(WaterfallStepContext sc, Exception e // clear state var state = await StateAccessor.GetAsync(sc.Context); - state.Clear(); + state.ClearLuisResult(); } protected TicketCard ConvertTicket(Ticket ticket) @@ -530,7 +681,11 @@ protected class Actions public const string SetUrgency = "SetUrgency"; public const string SetId = "SetId"; + public const string NavigateYesNoPrompt = "NavigateYesNoPrompt"; + public const string NavigateNoPrompt = "NavigateNoPrompt"; + public const string CreateTicket = "CreateTicket"; + public const string DisplayExisting = "DisplayExisting"; public const string UpdateTicket = "UpdateTicket"; public const string UpdateAttribute = "UpdateAttribute"; @@ -539,10 +694,12 @@ protected class Actions public const string ShowTicket = "ShowTicket"; public const string ShowAttribute = "ShowAttribute"; public const string ShowAttributePrompt = "ShowAttributePrompt"; + public const string ShowTicketLoop = "ShowTicketLoop"; public const string CloseTicket = "CloseTicket"; public const string ShowKnowledge = "ShowKnowledge"; + public const string ShowKnowledgeLoop = "ShowKnowledgeLoop"; } } } diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs index daa8cbb26e..e0cdb55368 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs @@ -94,8 +94,11 @@ public UpdateTicketDialog( } else { - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.ShowUpdates)); - await sc.Context.SendActivityAsync(sb.ToString()); + var token = new StringDictionary() + { + { "Attributes", sb.ToString() } + }; + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.ShowUpdates, token)); } return await sc.NextAsync(); @@ -104,6 +107,8 @@ public UpdateTicketDialog( protected async Task UpdateLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + + // state.AttributeType from Luis should be used first state.AttributeType = AttributeType.None; return await sc.ReplaceDialogAsync(Actions.UpdateAttribute); @@ -115,7 +120,7 @@ public UpdateTicketDialog( if (state.Token == null) { await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); - return await sc.EndDialogAsync(); + return await sc.CancelAllDialogsAsync(); } var management = ServiceManager.CreateManagement(Settings, state.Token); @@ -128,7 +133,7 @@ public UpdateTicketDialog( { "Error", result.ErrorMessage } }; await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); - return await sc.EndDialogAsync(); + return await sc.CancelAllDialogsAsync(); } var card = new Card() diff --git a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs index a9e69b7f0d..642b4d094a 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs @@ -12,11 +12,18 @@ public class SkillState { public SkillState() { - Clear(); + ClearLuisResult(); } + // always call GetAuthToken before using public TokenResponse Token { get; set; } + // handle manually + public int PageIndex { get; set; } + + // used when from ShowKnowledge to CreateTicket + public bool SkipDisplayExisting { get; set; } + public string Id { get; set; } public string TicketDescription { get; set; } @@ -27,9 +34,12 @@ public SkillState() public AttributeType AttributeType { get; set; } + // from OnInterruptDialogAsync + public GeneralLuis.Intent GeneralIntent { get; set; } + public void DigestLuisResult(ITSMLuis luis) { - Clear(); + ClearLuisResult(); if (luis.Entities.TicketDescription != null) { @@ -65,11 +75,14 @@ public void DigestLuisResult(ITSMLuis luis) AttributeType = AttributeType.None; } } + else if (topIntent == ITSMLuis.Intent.TicketCreate) + { + SkipDisplayExisting = false; + } } - public void Clear() + public void ClearLuisResult() { - Token = null; Id = null; TicketDescription = null; CloseReason = null; diff --git a/skills/src/csharp/experimental/itsmskill/Prompts/AttributeWithNoPrompt.cs b/skills/src/csharp/experimental/itsmskill/Prompts/AttributeWithNoPrompt.cs index afc3bc5a46..8d5f754515 100644 --- a/skills/src/csharp/experimental/itsmskill/Prompts/AttributeWithNoPrompt.cs +++ b/skills/src/csharp/experimental/itsmskill/Prompts/AttributeWithNoPrompt.cs @@ -1,4 +1,7 @@ -using System; +// 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; diff --git a/skills/src/csharp/experimental/itsmskill/Prompts/GeneralPrompt.cs b/skills/src/csharp/experimental/itsmskill/Prompts/GeneralPrompt.cs new file mode 100644 index 0000000000..f7f4847524 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Prompts/GeneralPrompt.cs @@ -0,0 +1,72 @@ +// 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 ITSMSkill.Models; +using Luis; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; + +namespace ITSMSkill.Prompts +{ + public class GeneralPrompt : Prompt + { + private readonly ISet intents; + private readonly IStatePropertyAccessor stateAccessor; + + public GeneralPrompt(string dialogId, ISet intents, IStatePropertyAccessor stateAccessor, PromptValidator validator = null, string defaultLocale = null) +: base(dialogId, validator) + { + this.intents = intents; + this.stateAccessor = stateAccessor; + DefaultLocale = defaultLocale; + } + + public string DefaultLocale { get; set; } + + protected override async Task OnPromptAsync(ITurnContext turnContext, IDictionary state, PromptOptions options, bool isRetry, CancellationToken cancellationToken = default(CancellationToken)) + { + if (turnContext == null) + { + throw new ArgumentNullException(nameof(turnContext)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (isRetry && options.RetryPrompt != null) + { + await turnContext.SendActivityAsync(options.RetryPrompt, cancellationToken).ConfigureAwait(false); + } + else if (options.Prompt != null) + { + await turnContext.SendActivityAsync(options.Prompt, cancellationToken).ConfigureAwait(false); + } + } + + protected override async Task> OnRecognizeAsync(ITurnContext turnContext, IDictionary state, PromptOptions options, CancellationToken cancellationToken = default(CancellationToken)) + { + if (turnContext == null) + { + throw new ArgumentNullException(nameof(turnContext)); + } + + var result = new PromptRecognizerResult(); + + var skillState = await stateAccessor.GetAsync(turnContext); + + if (intents.Contains(skillState.GeneralIntent)) + { + result.Succeeded = true; + result.Value = skillState.GeneralIntent; + } + + return await Task.FromResult(result); + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs index 38e8d4a920..6f88839d7b 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs @@ -12,7 +12,10 @@ namespace ITSMSkill.Responses.Knowledge public class KnowledgeResponses : IResponseIdCollection { // Generated accessors + public const string IfFindWanted = "IfFindWanted"; + public const string IfCreateTicket = "IfCreateTicket"; public const string KnowledgeShow = "KnowledgeShow"; + public const string KnowledgeEnd = "KnowledgeEnd"; public const string KnowledgeShowNone = "KnowledgeShowNone"; } } \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.json index bd77cb4361..591329c7f9 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.json @@ -1,9 +1,27 @@ { - "KnowledgeShow": { + "IfFindWanted": { "replies": [ { - "text": "Here are your search results. Let me know when you need my help.", - "speak": "Here are your search results. Let me know when you need my help." + "text": "Page {Page}. Do you find what you want? \"Go forward/previously\" to navigate.", + "speak": "Page {Page}. Do you find what you want? \"Go forward/previously\" to navigate." + } + ], + "inputHint": "expectingInput" + }, + "IfCreateTicket": { + "replies": [ + { + "text": "Do you want to create a ticket for this?", + "speak": "Do you want to create a ticket for this?" + } + ], + "inputHint": "expectingInput" + }, + "KnowledgeEnd": { + "replies": [ + { + "text": "Page {Page} does not contain anything, try \"Go previously\".", + "speak": "Page {Page} does not contain anything, try \"Go previously\"." } ], "inputHint": "acceptingInput" diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs index 1125878338..4771d858ac 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs @@ -26,8 +26,6 @@ public class SharedResponses : IResponseIdCollection public const string InputUrgency = "InputUrgency"; public const string ConfirmId = "ConfirmId"; public const string InputId = "InputId"; - public const string IfExistingSolve = "IfExistingSolve"; - public const string ExistingSolve = "ExistingSolve"; public const string ServiceFailed = "ServiceFailed"; } } \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json index d19f566caa..f944f06380 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json @@ -217,24 +217,6 @@ ], "inputHint": "expectingInput" }, - "IfExistingSolve": { - "replies": [ - { - "text": "Does one of these solve your problem?", - "speak": "Does one of these solve your problem?" - } - ], - "inputHint": "expectingInput" - }, - "ExistingSolve": { - "replies": [ - { - "text": "Cool, let me know when you need my help.", - "speak": "Cool, let me know when you need my help." - } - ], - "inputHint": "acceptingInput" - }, "ServiceFailed": { "replies": [ { diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs index 87aa41ba36..ec0fc62e99 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs @@ -12,6 +12,7 @@ namespace ITSMSkill.Responses.Ticket public class TicketResponses : IResponseIdCollection { // Generated accessors + public const string IfExistingSolve = "IfExistingSolve"; public const string TicketCreated = "TicketCreated"; public const string ConfirmUpdateAttribute = "ConfirmUpdateAttribute"; public const string UpdateAttribute = "UpdateAttribute"; @@ -22,6 +23,7 @@ public class TicketResponses : IResponseIdCollection public const string ShowUpdates = "ShowUpdates"; public const string ShowAttribute = "ShowAttribute"; public const string TicketShow = "TicketShow"; + public const string TicketEnd = "TicketEnd"; public const string TicketShowNone = "TicketShowNone"; public const string TicketClosed = "TicketClosed"; } diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json index 406b512b38..0b10c26fd7 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json @@ -1,4 +1,13 @@ { + "IfExistingSolve": { + "replies": [ + { + "text": "Page {Page}. Does one of these solve your problem? \"Go forward/previously\" to navigate.", + "speak": "Page {Page}. Does one of these solve your problem? \"Go forward/previously\" to navigate." + } + ], + "inputHint": "expectingInput" + }, "TicketCreated": { "replies": [ { @@ -49,8 +58,8 @@ "ShowConstraints": { "replies": [ { - "text": "You have the following constraints on search:", - "speak": "You have the following constraints on search:" + "text": "You have the following constraints on search:\n{Attributes}", + "speak": "You have the following constraints on search:\n{Attributes}" } ], "inputHint": "igoringInput" @@ -67,8 +76,8 @@ "ShowUpdates": { "replies": [ { - "text": "You have the following to update:", - "speak": "You have the following to update:" + "text": "You have the following to update:\n{Attributes}", + "speak": "You have the following to update:\n{Attributes}" } ], "inputHint": "igoringInput" @@ -87,8 +96,17 @@ "TicketShow": { "replies": [ { - "text": "Here are your search results. Let me know when you need my help.", - "speak": "Here are your search results. Let me know when you need my help." + "text": "Page {Page}. Do you want to see more search results? Try \"Go forward/previously\".", + "speak": "Page {Page}. Do you want to see more search results? Try \"Go forward/previously\"." + } + ], + "inputHint": "acceptingInput" + }, + "TicketEnd": { + "replies": [ + { + "text": "Page {Page} does not contain anything, try \"Go previously\".", + "speak": "Page {Page} does not contain anything, try \"Go previously\"." } ], "inputHint": "acceptingInput" diff --git a/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs b/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs index c612995180..e9c63ec131 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs @@ -9,12 +9,12 @@ public interface IITServiceManagement Task CreateTicket(string description, UrgencyLevel urgency); // like description & in urgencies & equal id & in states - Task SearchTicket(string description = null, List urgencies = null, string id = null, List states = null); + Task SearchTicket(int pageIndex, string description = null, List urgencies = null, string id = null, List states = null); Task UpdateTicket(string id, string description = null, UrgencyLevel urgency = UrgencyLevel.None); Task CloseTicket(string id, string reason); - Task SearchKnowledge(string query); + Task SearchKnowledge(string query, int pageIndex); } } diff --git a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs index d4c82a5011..a9c1c6aa0c 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs @@ -89,7 +89,7 @@ public async Task CreateTicket(string description, UrgencyLevel u } } - public async Task SearchTicket(string description = null, List urgencies = null, string id = null, List states = null) + public async Task SearchTicket(int pageIndex, string description = null, List urgencies = null, string id = null, List states = null) { try { @@ -127,6 +127,8 @@ public async Task SearchTicket(string description = null, List(request); return new TicketsResult() { @@ -211,7 +213,7 @@ public async Task CloseTicket(string id, string reason) } } - public async Task SearchKnowledge(string query) + public async Task SearchKnowledge(string query, int pageIndex) { var request = CreateRequest(KnowledgeResource); @@ -220,6 +222,8 @@ public async Task SearchKnowledge(string query) request.AddParameter("sysparm_limit", limitSize); + request.AddParameter("sysparm_offset", limitSize * pageIndex); + try { var result = await client.GetAsync(request); From b2aaca0e7bf516e6c30dde41b3eb444db8dfcb51 Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Wed, 21 Aug 2019 12:13:40 +0800 Subject: [PATCH 08/15] [ITSM] update for IWhitelistAuthenticationProvider, manifestTemplate, docs --- docs/_docs/reference/skills/experimental.md | 16 +++ skills/src/csharp/Skills.sln | 12 ++ .../itsmskill/Adapters/CustomSkillAdapter.cs | 2 +- .../itsmskill/Controllers/BotController.cs | 6 +- .../Deployment/Resources/LU/en/Ticket/Show.lu | 3 + .../experimental/itsmskill/ITSMSkill.csproj | 4 +- .../csharp/experimental/itsmskill/Startup.cs | 10 ++ .../itsmskill/manifestTemplate.json | 130 ++++++++++++++++-- .../csharp/experimental/itsmskill/readme.md | 13 +- 9 files changed, 170 insertions(+), 26 deletions(-) diff --git a/docs/_docs/reference/skills/experimental.md b/docs/_docs/reference/skills/experimental.md index a8aad4e70a..8d3e13be10 100644 --- a/docs/_docs/reference/skills/experimental.md +++ b/docs/_docs/reference/skills/experimental.md @@ -37,6 +37,22 @@ The Weather skill provides a basic Skill that integrates with [AccuWeather](http The Music skill integrates with [Spotify](https://developer.spotify.com/documentation/web-api/libraries/) to look up playlists and artists and open via the Spotify app. Provide credentials after you [create a Spotify client](https://developer.spotify.com/dashboard/) in the appsettings to configure the skill. +## IT Service Managerment Skill + +The [IT Service Managerment skill](https://github.com/microsoft/AI/tree/next/skills/src/csharp/experimental/itsmskill) provides a basic skill that provides ticket and knowledge base related capabilities and supports SerivceNow. + +To test this skill, one should setup the following: + +* Create a ServiceNow instance in [Developers](https://developer.servicenow.com/app.do#!/instance) and update the serviceNowUrl of appsettings.json +* Set up a scripted REST API for current user's sys_id following [this question](https://community.servicenow.com/community?id=community_question&sys_id=52efcb88db1ddb084816f3231f9619c7) and update the serviceNowGetUserId of appsetting.json + - Please raise an issue if simpler way is found +* Set up endpoint by [this document](https://docs.servicenow.com/bundle/london-platform-administration/page/administer/security/task/t_CreateEndpointforExternalClients.html#t_CreateEndpointforExternalClients) for Client id and Client secret to be used in the following OAuth Connection + - Redirect URL is https://token.botframework.com/.auth/web/redirect +* Add an OAuth Connection in the Settings of Web App Bot named 'ServiceNow' with Service Provider 'Generic Oauth 2' + - Authorization URL as https://instance.service-now.com/oauth_auth.do + - Token URL, Refresh URL as https://instance.service-now.com/oauth_token.do +* If one wants to use it in VA, add VA's appId to AppsWhitelist in the Startup.cs + ## Experimental Skill Deployment The Experimental Skills require the following dependencies for end to end operation which are created through an ARM script which you can modify as required. diff --git a/skills/src/csharp/Skills.sln b/skills/src/csharp/Skills.sln index a733c24260..4100eef1fe 100644 --- a/skills/src/csharp/Skills.sln +++ b/skills/src/csharp/Skills.sln @@ -43,6 +43,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MusicSkill", "experimental\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSkill", "experimental\eventskill\EventSkill.csproj", "{5BF2293A-6E56-464A-8355-EDC8972F7E09}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ITSMSkill", "experimental\itsmskill\ITSMSkill.csproj", "{3DEE3053-5DCC-4263-8692-2E96C7010997}" +EndProject + Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug - NuGet Packages|Any CPU = Debug - NuGet Packages|Any CPU @@ -187,6 +190,14 @@ Global {5BF2293A-6E56-464A-8355-EDC8972F7E09}.Documentation|Any CPU.Build.0 = Debug|Any CPU {5BF2293A-6E56-464A-8355-EDC8972F7E09}.Release|Any CPU.ActiveCfg = Release|Any CPU {5BF2293A-6E56-464A-8355-EDC8972F7E09}.Release|Any CPU.Build.0 = Release|Any CPU + {3DEE3053-5DCC-4263-8692-2E96C7010997}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU + {3DEE3053-5DCC-4263-8692-2E96C7010997}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU + {3DEE3053-5DCC-4263-8692-2E96C7010997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DEE3053-5DCC-4263-8692-2E96C7010997}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DEE3053-5DCC-4263-8692-2E96C7010997}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU + {3DEE3053-5DCC-4263-8692-2E96C7010997}.Documentation|Any CPU.Build.0 = Debug|Any CPU + {3DEE3053-5DCC-4263-8692-2E96C7010997}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DEE3053-5DCC-4263-8692-2E96C7010997}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -209,6 +220,7 @@ Global {0EFEA4F2-DC7E-436E-B951-E9B566AFF7F0} = {3665D242-1A88-4860-B148-BAB695B7B5E4} {A2ECB4BF-FD59-4746-B699-F1C326D561BB} = {3665D242-1A88-4860-B148-BAB695B7B5E4} {5BF2293A-6E56-464A-8355-EDC8972F7E09} = {3665D242-1A88-4860-B148-BAB695B7B5E4} + {3DEE3053-5DCC-4263-8692-2E96C7010997} = {3665D242-1A88-4860-B148-BAB695B7B5E4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7B849B7E-CCF5-4031-91F7-CA835433B457} diff --git a/skills/src/csharp/experimental/itsmskill/Adapters/CustomSkillAdapter.cs b/skills/src/csharp/experimental/itsmskill/Adapters/CustomSkillAdapter.cs index 5f2a524393..39583c9306 100644 --- a/skills/src/csharp/experimental/itsmskill/Adapters/CustomSkillAdapter.cs +++ b/skills/src/csharp/experimental/itsmskill/Adapters/CustomSkillAdapter.cs @@ -37,7 +37,7 @@ public CustomSkillAdapter( Use(new TelemetryLoggerMiddleware(telemetryClient, logPersonalInformation: true)); Use(new SetLocaleMiddleware(settings.DefaultLocale ?? "en-us")); Use(new EventDebuggerMiddleware()); - Use(new SkillMiddleware(userState, conversationState, conversationState.CreateProperty(nameof(ITSMSkill)))); + Use(new SkillMiddleware(userState, conversationState, conversationState.CreateProperty(nameof(DialogState)))); } } } diff --git a/skills/src/csharp/experimental/itsmskill/Controllers/BotController.cs b/skills/src/csharp/experimental/itsmskill/Controllers/BotController.cs index 6be6a25462..5fb83848fc 100644 --- a/skills/src/csharp/experimental/itsmskill/Controllers/BotController.cs +++ b/skills/src/csharp/experimental/itsmskill/Controllers/BotController.cs @@ -5,6 +5,7 @@ using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Builder.Skills.Auth; using Microsoft.Bot.Builder.Solutions; namespace ITSMSkill.Controllers @@ -16,8 +17,9 @@ public BotController( IBot bot, BotSettingsBase botSettings, IBotFrameworkHttpAdapter botFrameworkHttpAdapter, - SkillWebSocketAdapter skillWebSocketAdapter) - : base(bot, botSettings, botFrameworkHttpAdapter, skillWebSocketAdapter) + SkillWebSocketAdapter skillWebSocketAdapter, + IWhitelistAuthenticationProvider whitelistAuthenticationProvider) + : base(bot, botSettings, botFrameworkHttpAdapter, skillWebSocketAdapter, whitelistAuthenticationProvider) { } } diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Show.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Show.lu index ddfc6f0b31..1df648ff07 100644 --- a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Show.lu +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Show.lu @@ -15,3 +15,6 @@ - i would like to see my tickets - i would like to see urgency low issues - i would like to see incidents about {TicketDescription=unable to connect} +- search tickets +- search issues +- search incidents diff --git a/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj index c59de510dc..76b5faa141 100644 --- a/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj +++ b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj @@ -52,8 +52,8 @@ - - + + diff --git a/skills/src/csharp/experimental/itsmskill/Startup.cs b/skills/src/csharp/experimental/itsmskill/Startup.cs index bce7a9120d..c6437d7795 100644 --- a/skills/src/csharp/experimental/itsmskill/Startup.cs +++ b/skills/src/csharp/experimental/itsmskill/Startup.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; using System.Linq; using ITSMSkill.Adapters; using ITSMSkill.Bots; @@ -21,6 +22,7 @@ using Microsoft.Bot.Builder.Integration.ApplicationInsights.Core; using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Builder.Skills.Auth; using Microsoft.Bot.Builder.Solutions; using Microsoft.Bot.Builder.Solutions.Responses; using Microsoft.Bot.Builder.Solutions.TaskExtensions; @@ -119,6 +121,8 @@ public void ConfigureServices(IServiceCollection services) // Configure bot services.AddTransient>(); + + services.AddSingleton(new SimpleWhitelistAuthenticationProvider()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -135,5 +139,11 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) .UseWebSockets() .UseMvc(); } + + public class SimpleWhitelistAuthenticationProvider : IWhitelistAuthenticationProvider + { + // set VA appid here + public List AppsWhitelist => new List { }; + } } } \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/manifestTemplate.json b/skills/src/csharp/experimental/itsmskill/manifestTemplate.json index 1dbc6d1024..812b929629 100644 --- a/skills/src/csharp/experimental/itsmskill/manifestTemplate.json +++ b/skills/src/csharp/experimental/itsmskill/manifestTemplate.json @@ -1,23 +1,135 @@ { - "id": "", - "name": "", - "description": "", + "id": "itsmSkill", + "name": "ITSM Skill", + "description": "The IT Service Management Skill provides ticket and knowledge base related capabilities and supports SerivceNow.", "iconUrl": "", "authenticationConnections": [ { - "id": "", - "serviceProviderId": "", + "id": "ServiceNow", + "serviceProviderId": "Generic Oauth 2", "scopes": "" } ], "actions": [ { - "id": "", + "id": "itsmSkill_createTicket", "definition": { - "description": "", + "description": "Create a new ticket", "slots": [ { - "name": "", + "name": "ticketDescription", + "types": [ "string" ] + }, + { + "name": "urgencyLevel", + "types": [ "string" ] + } + ], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "ITSM#TicketCreate" + ] + } + ] + } + } + }, + { + "id": "itsmSkill_updateTicket", + "definition": { + "description": "Update an existing ticket", + "slots": [ + { + "name": "ticketDescription", + "types": [ "string" ] + }, + { + "name": "urgencyLevel", + "types": [ "string" ] + }, + { + "name": "attributeType", + "types": [ "string" ] + } + ], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "ITSM#TicketUpdate" + ] + } + ] + } + } + }, + { + "id": "itsmSkill_showTicket", + "definition": { + "description": "Show tickets matching constraints", + "slots": [ + { + "name": "ticketDescription", + "types": [ "string" ] + }, + { + "name": "urgencyLevel", + "types": [ "string" ] + }, + { + "name": "id", + "types": [ "string" ] + } + ], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "ITSM#TicketShow" + ] + } + ] + } + } + }, + { + "id": "itsmSkill_closeTicket", + "definition": { + "description": "Close ticket with reason", + "slots": [ + { + "name": "closeReason", + "types": [ "string" ] + }, + { + "name": "id", + "types": [ "string" ] + } + ], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "ITSM#TicketClose" + ] + } + ] + } + } + }, + { + "id": "itsmSkill_showKnowledge", + "definition": { + "description": "Show knowledge base matching constraints", + "slots": [ + { + "name": "ticketDescription", "types": [ "string" ] } ], @@ -26,7 +138,7 @@ { "locale": "en", "source": [ - "luisModel#intent" + "ITSM#KnowledgeShow" ] } ] diff --git a/skills/src/csharp/experimental/itsmskill/readme.md b/skills/src/csharp/experimental/itsmskill/readme.md index 7a05ba96a9..63a5b76a4a 100644 --- a/skills/src/csharp/experimental/itsmskill/readme.md +++ b/skills/src/csharp/experimental/itsmskill/readme.md @@ -1,14 +1,3 @@ # IT Service Managerment Experimental Skill -To test this skill, one has to setup the following: - -* Create a ServiceNow instance in https://developer.servicenow.com/app.do#!/instance and update the serviceNowUrl of appsettings.json -* Set up a scripted REST API for current user's sys_id following https://community.servicenow.com/community?id=community_question&sys_id=52efcb88db1ddb084816f3231f9619c7 and update the serviceNowGetUserId of appsetting.json - - Please raise an issue if simpler way is found -* Set up endpoint (https://docs.servicenow.com/bundle/london-platform-administration/page/administer/security/task/t_CreateEndpointforExternalClients.html#t_CreateEndpointforExternalClients) for Client id and Client secret in the following OAuth Connection - - Redirect URL is https://token.botframework.com/.auth/web/redirect -* Add an OAuth Connection in the Settings of Web App Bot named 'ServiceNow' with Service Provider 'Generic Oauth 2' - - Authorization URL as https://instance.service-now.com/oauth_auth.do - - Token URL, Refresh URL as https://instance.service-now.com/oauth_token.do - -Once this skill is done, these will be moved into the Experimental Skill [documentation page](/docs/reference/skills/experimental.md). +See the Experimental Skill [documentation page](/docs/reference/skills/experimental.md) for information on how to deploy and test this Skill. From fe62ece46cb2c619e37b8ba458f3e559b191df21 Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Fri, 23 Aug 2019 16:59:16 +0800 Subject: [PATCH 09/15] [ITSM] add top login prompt, additional sources, TicketState * additional sources should be removed when published * add TicketState list entity and related updates --- .../Deployment/Resources/LU/en/ITSM.lu | 17 +++ .../itsmskill/Dialogs/CloseTicketDialog.cs | 6 - .../itsmskill/Dialogs/CreateTicketDialog.cs | 6 - .../itsmskill/Dialogs/MainDialog.cs | 4 +- .../itsmskill/Dialogs/ShowTicketDialog.cs | 20 ++- .../itsmskill/Dialogs/SkillDialogBase.cs | 124 +++++++++++++++++- .../itsmskill/Dialogs/UpdateTicketDialog.cs | 6 - .../experimental/itsmskill/ITSMSkill.csproj | 3 + .../itsmskill/Models/AttributeType.cs | 4 +- .../itsmskill/Models/SkillState.cs | 16 ++- .../Responses/Shared/SharedResponses.cs | 2 + .../Responses/Shared/SharedResponses.json | 18 +++ .../Shared/SharedStrings.Designer.cs | 9 ++ .../Responses/Shared/SharedStrings.resx | 3 + .../Responses/Ticket/TicketResponses.json | 2 +- .../itsmskill/Services/ITSMLuis.cs | 17 ++- 16 files changed, 219 insertions(+), 38 deletions(-) diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu index f80e551037..fe950a862a 100644 --- a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu @@ -31,8 +31,25 @@ $AttributeType:urgency= $AttributeType:description= +$AttributeType:state= + $UrgencyLevel:low= $UrgencyLevel:medium= $UrgencyLevel:high= +- urgent + +$TicketState:new= + +$TicketState:inprogress= +- in progress + +$TicketState:onhold= +- on hold + +$TicketState:resolved= + +$TicketState:closed= + +$TicketState:canceled= diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs index 65df37e9f9..838da6af48 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs @@ -52,12 +52,6 @@ public CloseTicketDialog( protected async Task CloseTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - if (state.Token == null) - { - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); - return await sc.CancelAllDialogsAsync(); - } - var management = ServiceManager.CreateManagement(Settings, state.Token); var result = await management.CloseTicket(id: state.Id, reason: state.CloseReason); diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs index e09962498a..eee46cd5cc 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs @@ -86,12 +86,6 @@ public CreateTicketDialog( protected async Task CreateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - if (state.Token == null) - { - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); - return await sc.CancelAllDialogsAsync(); - } - var management = ServiceManager.CreateManagement(Settings, state.Token); var result = await management.CreateTicket(state.TicketDescription, state.UrgencyLevel); diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs index f69a9fdeca..203540d5f7 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs @@ -90,10 +90,10 @@ public MainDialog( var result = await luisService.RecognizeAsync(dc.Context, CancellationToken.None); var intent = result?.TopIntent().intent; - if (intent != ITSMLuis.Intent.None) + if (intent != null && intent != ITSMLuis.Intent.None) { var state = await _stateAccessor.GetAsync(dc.Context, () => new SkillState()); - state.DigestLuisResult(result); + state.DigestLuisResult(result, (ITSMLuis.Intent)intent); } switch (intent) diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs index e2471a86d8..cc8fa7d970 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs @@ -60,7 +60,7 @@ public ShowTicketDialog( IfContinueShow }; - var attributesForShow = new AttributeType[] { AttributeType.Id, AttributeType.Description, AttributeType.Urgency }; + var attributesForShow = new AttributeType[] { AttributeType.Id, AttributeType.Description, AttributeType.Urgency, AttributeType.State }; AddDialog(new WaterfallDialog(Actions.ShowTicket, showTicket) { TelemetryClient = telemetryClient }); AddDialog(new WaterfallDialog(Actions.ShowAttribute, showAttribute) { TelemetryClient = telemetryClient }); @@ -111,6 +111,11 @@ public ShowTicketDialog( sb.AppendLine($"{SharedStrings.Urgency}{state.UrgencyLevel.ToLocalizedString()}"); } + if (state.TicketState != TicketState.None) + { + sb.AppendLine($"{SharedStrings.TicketState}{state.TicketState.ToLocalizedString()}"); + } + if (sb.Length == 0) { await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.ShowConstraintNone)); @@ -136,11 +141,6 @@ public ShowTicketDialog( protected async Task ShowTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - if (state.Token == null) - { - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); - return await sc.CancelAllDialogsAsync(); - } bool firstDisplay = false; if (state.PageIndex == -1) @@ -156,7 +156,13 @@ public ShowTicketDialog( urgencies.Add(state.UrgencyLevel); } - var result = await management.SearchTicket(state.PageIndex, description: state.TicketDescription, urgencies: urgencies, id: state.Id); + var states = new List(); + if (state.TicketState != TicketState.None) + { + states.Add(state.TicketState); + } + + var result = await management.SearchTicket(state.PageIndex, description: state.TicketDescription, urgencies: urgencies, id: state.Id, states: states); if (!result.Success) { diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs index 59e7290848..2c00f930fd 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs @@ -76,6 +76,20 @@ public SkillDialogBase( SetId }; + var setState = new WaterfallStep[] + { + CheckState, + InputState, + SetState + }; + + var baseAuth = new WaterfallStep[] + { + GetAuthToken, + AfterGetAuthToken, + BeginInitialDialog + }; + var navigateYesNo = new HashSet() { GeneralLuis.Intent.ShowNext, @@ -97,8 +111,12 @@ public SkillDialogBase( AddDialog(new WaterfallDialog(Actions.SetDescription, setDescription)); AddDialog(new WaterfallDialog(Actions.SetUrgency, setUrgency)); AddDialog(new WaterfallDialog(Actions.SetId, setId)); + AddDialog(new WaterfallDialog(Actions.SetState, setState)); + AddDialog(new WaterfallDialog(Actions.BaseAuth, baseAuth)); AddDialog(new GeneralPrompt(Actions.NavigateYesNoPrompt, navigateYesNo, StateAccessor)); AddDialog(new GeneralPrompt(Actions.NavigateNoPrompt, navigateNo, StateAccessor)); + + base.InitialDialogId = Actions.BaseAuth; } protected BotSettings Settings { get; set; } @@ -111,6 +129,8 @@ public SkillDialogBase( protected IServiceManager ServiceManager { get; set; } + protected new string InitialDialogId { get; set; } + protected string ConfirmAttributeResponse { get; set; } protected string InputAttributeResponse { get; set; } @@ -172,6 +192,12 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can state.Token = null; } + if (state.Token == null) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); + return await sc.CancelAllDialogsAsync(); + } + return await sc.NextAsync(); } catch (SkillException ex) @@ -186,6 +212,11 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can } } + protected async Task BeginInitialDialog(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + return await sc.BeginDialogAsync(InitialDialogId); + } + protected async Task CheckId(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); @@ -292,16 +323,24 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); if (state.AttributeType == AttributeType.Description) { + state.TicketDescription = null; return await sc.BeginDialogAsync(Actions.SetDescription); } else if (state.AttributeType == AttributeType.Urgency) { + state.UrgencyLevel = UrgencyLevel.None; return await sc.BeginDialogAsync(Actions.SetUrgency); } else if (state.AttributeType == AttributeType.Id) { + state.Id = null; return await sc.BeginDialogAsync(Actions.SetId); } + else if (state.AttributeType == AttributeType.State) + { + state.TicketState = TicketState.None; + return await sc.BeginDialogAsync(Actions.SetState); + } else { throw new Exception($"Invalid AttributeType: {state.AttributeType}"); @@ -471,14 +510,88 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can return await sc.NextAsync(); } - protected async Task ShowKnowledge(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + protected async Task CheckState(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - if (state.Token == null) + if (state.TicketState == TicketState.None) { - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); - return await sc.CancelAllDialogsAsync(); + return await sc.NextAsync(false); } + else + { + var replacements = new StringDictionary + { + { "State", state.TicketState.ToString() } + }; + + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.ConfirmState, replacements) + }; + + return await sc.PromptAsync(nameof(ConfirmPrompt), options); + } + } + + protected async Task InputState(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (!(bool)sc.Result || state.TicketState == TicketState.None) + { + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.InputState), + Choices = new List() + { + new Choice() + { + Value = TicketState.New.ToLocalizedString() + }, + new Choice() + { + Value = TicketState.InProgress.ToLocalizedString() + }, + new Choice() + { + Value = TicketState.OnHold.ToLocalizedString() + }, + new Choice() + { + Value = TicketState.Resolved.ToLocalizedString() + }, + new Choice() + { + Value = TicketState.Closed.ToLocalizedString() + }, + new Choice() + { + Value = TicketState.Canceled.ToLocalizedString() + } + } + }; + + return await sc.PromptAsync(nameof(ChoicePrompt), options); + } + else + { + // use Index to skip localization + return await sc.NextAsync(new FoundChoice() + { + Index = (int)state.TicketState - 1 + }); + } + } + + protected async Task SetState(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.TicketState = (TicketState)(((FoundChoice)sc.Result).Index + 1); + return await sc.NextAsync(); + } + + protected async Task ShowKnowledge(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); bool firstDisplay = false; if (state.PageIndex == -1) @@ -680,6 +793,9 @@ protected class Actions public const string SetDescription = "SetDescription"; public const string SetUrgency = "SetUrgency"; public const string SetId = "SetId"; + public const string SetState = "SetState"; + + public const string BaseAuth = "BaseAuth"; public const string NavigateYesNoPrompt = "NavigateYesNoPrompt"; public const string NavigateNoPrompt = "NavigateNoPrompt"; diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs index e0cdb55368..6b41466103 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs @@ -117,12 +117,6 @@ public UpdateTicketDialog( protected async Task UpdateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - if (state.Token == null) - { - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.AuthFailed)); - return await sc.CancelAllDialogsAsync(); - } - var management = ServiceManager.CreateManagement(Settings, state.Token); var result = await management.UpdateTicket(state.Id, state.TicketDescription, state.UrgencyLevel); diff --git a/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj index 76b5faa141..398e00909d 100644 --- a/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj +++ b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj @@ -3,6 +3,9 @@ netcoreapp2.2 NU1701 + + https://botbuilder.myget.org/F/aitemplates/api/v3/index.json; + diff --git a/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs b/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs index b9ae102648..668c760ea9 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs @@ -11,6 +11,8 @@ public enum AttributeType [EnumLocalizedDescription("AttributeDescription", typeof(SharedStrings))] Description, [EnumLocalizedDescription("AttributeUrgency", typeof(SharedStrings))] - Urgency + Urgency, + [EnumLocalizedDescription("AttributeState", typeof(SharedStrings))] + State, } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs index 642b4d094a..0cef2a2f4c 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs @@ -34,10 +34,12 @@ public SkillState() public AttributeType AttributeType { get; set; } + public TicketState TicketState { get; set; } + // from OnInterruptDialogAsync public GeneralLuis.Intent GeneralIntent { get; set; } - public void DigestLuisResult(ITSMLuis luis) + public void DigestLuisResult(ITSMLuis luis, ITSMLuis.Intent topIntent) { ClearLuisResult(); @@ -62,7 +64,12 @@ public void DigestLuisResult(ITSMLuis luis) AttributeType = Enum.Parse(luis.Entities.AttributeType[0][0], true); } - var topIntent = luis.TopIntent().intent; + if (luis.Entities.TicketState != null) + { + TicketState = Enum.Parse(luis.Entities.TicketState[0][0], true); + } + + // TODO some special digestions if (topIntent == ITSMLuis.Intent.TicketUpdate) { // clear AttributeType if already set @@ -79,6 +86,10 @@ public void DigestLuisResult(ITSMLuis luis) { SkipDisplayExisting = false; } + else if (topIntent == ITSMLuis.Intent.TicketShow) + { + AttributeType = AttributeType.None; + } } public void ClearLuisResult() @@ -88,6 +99,7 @@ public void ClearLuisResult() CloseReason = null; UrgencyLevel = UrgencyLevel.None; AttributeType = AttributeType.None; + TicketState = TicketState.None; } } } diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs index 4771d858ac..2baf09a15a 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs @@ -24,6 +24,8 @@ public class SharedResponses : IResponseIdCollection public const string InputReason = "InputReason"; public const string ConfirmUrgency = "ConfirmUrgency"; public const string InputUrgency = "InputUrgency"; + public const string ConfirmState = "ConfirmState"; + public const string InputState = "InputState"; public const string ConfirmId = "ConfirmId"; public const string InputId = "InputId"; public const string ServiceFailed = "ServiceFailed"; diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json index f944f06380..198b4f2732 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json @@ -199,6 +199,24 @@ ], "inputHint": "expectingInput" }, + "ConfirmState": { + "replies": [ + { + "text": "Is the ticket state \"{State}\" correct?", + "speak": "Is the ticket state \"{State}\" correct?" + } + ], + "inputHint": "expectingInput" + }, + "InputState": { + "replies": [ + { + "text": "Please input the ticket state:", + "speak": "Please input the ticket state:" + } + ], + "inputHint": "expectingInput" + }, "ConfirmId": { "replies": [ { diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs index ffa1e0b9dc..93377f64d1 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs @@ -78,6 +78,15 @@ public static string AttributeId { } } + /// + /// Looks up a localized string similar to state. + /// + public static string AttributeState { + get { + return ResourceManager.GetString("AttributeState", resourceCulture); + } + } + /// /// Looks up a localized string similar to urgency. /// diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx index b03edf5853..c255ad16ca 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx @@ -171,4 +171,7 @@ ID: + + state + \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json index 0b10c26fd7..02554b41fb 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json @@ -84,7 +84,7 @@ }, "ShowAttribute": { // same with SharedStrings - "suggestedActions": [ "id", "description", "urgency", "no" ], + "suggestedActions": [ "id", "description", "urgency", "state", "no" ], "replies": [ { "text": "What attribute do you want to set as search constraint? Or \"No\" for no more attribute.", diff --git a/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs index 6231461c1b..aae29a3a15 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs @@ -12,8 +12,12 @@ namespace Luis { public partial class ITSMLuis: IRecognizerConvert { + [JsonProperty("text")] public string Text; + + [JsonProperty("alteredText")] public string AlteredText; + public enum Intent { None, TicketCreate, @@ -22,29 +26,36 @@ public enum Intent { TicketClose, KnowledgeShow }; + [JsonProperty("intents")] public Dictionary Intents; public class _Entities { // Simple entities - public string[] TicketDescription; public string[] CloseReason; + public string[] TicketDescription; + // Lists public string[][] AttributeType; + + public string[][] TicketState; + public string[][] UrgencyLevel; // Instance public class _Instance { - public InstanceData[] TicketDescription; - public InstanceData[] CloseReason; public InstanceData[] AttributeType; + public InstanceData[] CloseReason; + public InstanceData[] TicketDescription; + public InstanceData[] TicketState; public InstanceData[] UrgencyLevel; } [JsonProperty("$instance")] public _Instance _instance; } + [JsonProperty("entities")] public _Entities Entities; [JsonExtensionData(ReadData = true, WriteData = true)] From 5ff0b4cf132ea131dedc013354b9cb202ff501ca Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Mon, 26 Aug 2019 13:33:15 +0800 Subject: [PATCH 10/15] [ITSM] use ticket number instead of id * add TicketNumber entity * add TicketNumberPrompt * update card jsons * update routines of UpdateTicketDialog & CloseTicketDialog --- .../itsmskill/Content/Ticket.1.0.json | 2 +- .../itsmskill/Content/Ticket.json | 2 +- .../Deployment/Resources/LU/en/ITSM.lu | 2 + .../itsmskill/Dialogs/CloseTicketDialog.cs | 25 ++-- .../itsmskill/Dialogs/CreateTicketDialog.cs | 15 +-- .../itsmskill/Dialogs/ShowKnowledgeDialog.cs | 2 +- .../itsmskill/Dialogs/ShowTicketDialog.cs | 15 +-- .../itsmskill/Dialogs/SkillDialogBase.cs | 113 ++++++++++++++++-- .../itsmskill/Dialogs/UpdateTicketDialog.cs | 18 +-- .../itsmskill/Models/AttributeType.cs | 2 + .../Models/ServiceNow/TicketResponse.cs | 2 + .../itsmskill/Models/SkillState.cs | 15 ++- .../experimental/itsmskill/Models/Ticket.cs | 2 + .../itsmskill/Models/TicketCard.cs | 2 + .../itsmskill/Prompts/TicketNumberPrompt.cs | 94 +++++++++++++++ .../Responses/Shared/SharedResponses.cs | 1 + .../Responses/Shared/SharedResponses.json | 9 ++ .../Shared/SharedStrings.Designer.cs | 18 +++ .../Responses/Shared/SharedStrings.resx | 6 + .../Responses/Ticket/TicketResponses.cs | 4 + .../Responses/Ticket/TicketResponses.json | 38 +++++- .../Services/IITServiceManagement.cs | 2 +- .../itsmskill/Services/ITSMLuis.cs | 4 + .../Services/ServiceNow/Management.cs | 10 +- 24 files changed, 349 insertions(+), 54 deletions(-) create mode 100644 skills/src/csharp/experimental/itsmskill/Prompts/TicketNumberPrompt.cs diff --git a/skills/src/csharp/experimental/itsmskill/Content/Ticket.1.0.json b/skills/src/csharp/experimental/itsmskill/Content/Ticket.1.0.json index 7a309db31a..18bf7652f8 100644 --- a/skills/src/csharp/experimental/itsmskill/Content/Ticket.1.0.json +++ b/skills/src/csharp/experimental/itsmskill/Content/Ticket.1.0.json @@ -64,7 +64,7 @@ "type": "TextBlock", "size": "Small", "color": "Default", - "text": "{Id}" + "text": "{Number}" } ] }, diff --git a/skills/src/csharp/experimental/itsmskill/Content/Ticket.json b/skills/src/csharp/experimental/itsmskill/Content/Ticket.json index 376767eebf..4ae89240f1 100644 --- a/skills/src/csharp/experimental/itsmskill/Content/Ticket.json +++ b/skills/src/csharp/experimental/itsmskill/Content/Ticket.json @@ -79,7 +79,7 @@ "type": "TextBlock", "size": "Small", "color": "Light", - "text": "{Id}" + "text": "{Number}" } ] }, diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu index fe950a862a..81204ebdd8 100644 --- a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu @@ -53,3 +53,5 @@ $TicketState:resolved= $TicketState:closed= $TicketState:canceled= + +$TicketNumber:/INC[0-9]{7}/ diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs index 838da6af48..673277a31f 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs @@ -33,9 +33,8 @@ public CloseTicketDialog( { var closeTicket = new WaterfallStep[] { - CheckId, - InputId, - SetId, + BeginSetNumberThenId, + CheckClosed, CheckReason, InputReason, SetReason, @@ -49,6 +48,19 @@ public CloseTicketDialog( InitialDialogId = Actions.CloseTicket; } + protected async Task CheckClosed(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + + if (state.TicketTarget.State == TicketState.Closed) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.TicketAlreadyClosed)); + return await sc.EndDialogAsync(); + } + + return await sc.NextAsync(); + } + protected async Task CloseTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); @@ -57,12 +69,7 @@ public CloseTicketDialog( if (!result.Success) { - var errorReplacements = new StringDictionary - { - { "Error", result.ErrorMessage } - }; - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); - return await sc.CancelAllDialogsAsync(); + return await SendServiceErrorAndCancel(sc, result); } var card = new Card() diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs index eee46cd5cc..7e2c628bf5 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs @@ -72,14 +72,14 @@ public CreateTicketDialog( { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - if (state.SkipDisplayExisting) + if (state.DisplayExisting) { - return await sc.NextAsync(); + state.PageIndex = -1; + return await sc.BeginDialogAsync(Actions.DisplayExisting); } else { - state.PageIndex = -1; - return await sc.BeginDialogAsync(Actions.DisplayExisting); + return await sc.NextAsync(); } } @@ -91,12 +91,7 @@ public CreateTicketDialog( if (!result.Success) { - var errorReplacements = new StringDictionary - { - { "Error", result.ErrorMessage } - }; - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); - return await sc.CancelAllDialogsAsync(); + return await SendServiceErrorAndCancel(sc, result); } var card = new Card() diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs index 03a3dda87f..e94a36f452 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs @@ -88,7 +88,7 @@ public ShowKnowledgeDialog( if ((bool)sc.Result) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); - state.SkipDisplayExisting = true; + state.DisplayExisting = false; return await sc.ReplaceDialogAsync(nameof(CreateTicketDialog)); } diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs index cc8fa7d970..7c211909ca 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs @@ -60,7 +60,7 @@ public ShowTicketDialog( IfContinueShow }; - var attributesForShow = new AttributeType[] { AttributeType.Id, AttributeType.Description, AttributeType.Urgency, AttributeType.State }; + var attributesForShow = new AttributeType[] { AttributeType.Number, AttributeType.Description, AttributeType.Urgency, AttributeType.State }; AddDialog(new WaterfallDialog(Actions.ShowTicket, showTicket) { TelemetryClient = telemetryClient }); AddDialog(new WaterfallDialog(Actions.ShowAttribute, showAttribute) { TelemetryClient = telemetryClient }); @@ -96,9 +96,9 @@ public ShowTicketDialog( state.AttributeType = AttributeType.None; var sb = new StringBuilder(); - if (!string.IsNullOrEmpty(state.Id)) + if (!string.IsNullOrEmpty(state.TicketNumber)) { - sb.AppendLine($"{SharedStrings.ID}{state.Id}"); + sb.AppendLine($"{SharedStrings.TicketNumber}{state.TicketNumber}"); } if (!string.IsNullOrEmpty(state.TicketDescription)) @@ -162,16 +162,11 @@ public ShowTicketDialog( states.Add(state.TicketState); } - var result = await management.SearchTicket(state.PageIndex, description: state.TicketDescription, urgencies: urgencies, id: state.Id, states: states); + var result = await management.SearchTicket(state.PageIndex, description: state.TicketDescription, urgencies: urgencies, number: state.TicketNumber, states: states); if (!result.Success) { - var errorReplacements = new StringDictionary - { - { "Error", result.ErrorMessage } - }; - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); - return await sc.CancelAllDialogsAsync(); + return await SendServiceErrorAndCancel(sc, result); } if (result.Tickets == null || result.Tickets.Length == 0) diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs index 2c00f930fd..1141ce5637 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs @@ -12,6 +12,7 @@ using ITSMSkill.Models; using ITSMSkill.Prompts; using ITSMSkill.Responses.Shared; +using ITSMSkill.Responses.Ticket; using ITSMSkill.Services; using ITSMSkill.Utilities; using Luis; @@ -83,6 +84,22 @@ public SkillDialogBase( SetState }; + // TODO since number is ServiceNow specific regex, no need to check + var setNumber = new WaterfallStep[] + { + InputTicketNumber, + SetTicketNumber, + }; + + var setNumberThenId = new WaterfallStep[] + { + InputTicketNumber, + SetTicketNumber, + GetAuthToken, + AfterGetAuthToken, + SetIdFromNumber, + }; + var baseAuth = new WaterfallStep[] { GetAuthToken, @@ -108,10 +125,13 @@ public SkillDialogBase( AddDialog(new TextPrompt(nameof(TextPrompt))); AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt))); AddDialog(new ChoicePrompt(nameof(ChoicePrompt))); + AddDialog(new TicketNumberPrompt(nameof(TicketNumberPrompt))); AddDialog(new WaterfallDialog(Actions.SetDescription, setDescription)); AddDialog(new WaterfallDialog(Actions.SetUrgency, setUrgency)); AddDialog(new WaterfallDialog(Actions.SetId, setId)); AddDialog(new WaterfallDialog(Actions.SetState, setState)); + AddDialog(new WaterfallDialog(Actions.SetNumber, setNumber)); + AddDialog(new WaterfallDialog(Actions.SetNumberThenId, setNumberThenId)); AddDialog(new WaterfallDialog(Actions.BaseAuth, baseAuth)); AddDialog(new GeneralPrompt(Actions.NavigateYesNoPrompt, navigateYesNo, StateAccessor)); AddDialog(new GeneralPrompt(Actions.NavigateNoPrompt, navigateNo, StateAccessor)); @@ -341,6 +361,11 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can state.TicketState = TicketState.None; return await sc.BeginDialogAsync(Actions.SetState); } + else if (state.AttributeType == AttributeType.Number) + { + state.TicketNumber = null; + return await sc.BeginDialogAsync(Actions.SetNumber); + } else { throw new Exception($"Invalid AttributeType: {state.AttributeType}"); @@ -443,6 +468,72 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can return await sc.NextAsync(); } + protected async Task InputTicketNumber(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (string.IsNullOrEmpty(state.TicketNumber)) + { + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(SharedResponses.InputTicketNumber) + }; + + return await sc.PromptAsync(nameof(TicketNumberPrompt), options); + } + else + { + return await sc.NextAsync(state.TicketNumber); + } + } + + protected async Task SetTicketNumber(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.TicketNumber = (string)sc.Result; + return await sc.NextAsync(); + } + + protected async Task SetIdFromNumber(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + var management = ServiceManager.CreateManagement(Settings, state.Token); + var result = await management.SearchTicket(0, number: state.TicketNumber); + + if (!result.Success) + { + return await SendServiceErrorAndCancel(sc, result); + } + + if (result.Tickets == null || result.Tickets.Length == 0) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.TicketShowNone)); + return await sc.CancelAllDialogsAsync(); + } + + if (result.Tickets.Length >= 2) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.TicketDuplicateNumber)); + return await sc.CancelAllDialogsAsync(); + } + + state.TicketTarget = result.Tickets[0]; + state.Id = state.TicketTarget.Id; + + var card = new Card() + { + Name = GetDivergedCardName(sc.Context, "Ticket"), + Data = ConvertTicket(state.TicketTarget) + }; + + await sc.Context.SendActivityAsync(ResponseManager.GetCardResponse(TicketResponses.TicketTarget, card, null)); + return await sc.NextAsync(); + } + + protected async Task BeginSetNumberThenId(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + return await sc.BeginDialogAsync(Actions.SetNumberThenId); + } + protected async Task CheckUrgency(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); @@ -605,12 +696,7 @@ protected async Task GetAuthToken(WaterfallStepContext sc, Can if (!result.Success) { - var errorReplacements = new StringDictionary - { - { "Error", result.ErrorMessage } - }; - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); - return await sc.CancelAllDialogsAsync(); + return await SendServiceErrorAndCancel(sc, result); } if (result.Knowledges == null || result.Knowledges.Length == 0) @@ -748,6 +834,16 @@ protected async Task HandleDialogExceptions(WaterfallStepContext sc, Exception e state.ClearLuisResult(); } + protected async Task SendServiceErrorAndCancel(WaterfallStepContext sc, ResultBase result) + { + var errorReplacements = new StringDictionary + { + { "Error", result.ErrorMessage } + }; + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); + return await sc.CancelAllDialogsAsync(); + } + protected TicketCard ConvertTicket(Ticket ticket) { var card = new TicketCard() @@ -758,7 +854,8 @@ protected TicketCard ConvertTicket(Ticket ticket) OpenedTime = $"{SharedStrings.OpenedAt}{ticket.OpenedTime.ToString()}", Id = $"{SharedStrings.ID}{ticket.Id}", ResolvedReason = ticket.ResolvedReason, - Speak = ticket.Description + Speak = ticket.Description, + Number = $"{SharedStrings.TicketNumber}{ticket.Number}", }; return card; } @@ -794,6 +891,8 @@ protected class Actions public const string SetUrgency = "SetUrgency"; public const string SetId = "SetId"; public const string SetState = "SetState"; + public const string SetNumber = "SetNumber"; + public const string SetNumberThenId = "SetNumberThenId"; public const string BaseAuth = "BaseAuth"; diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs index 6b41466103..f2692fb155 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs @@ -36,9 +36,7 @@ public UpdateTicketDialog( { var updateTicket = new WaterfallStep[] { - CheckId, - InputId, - SetId, + BeginSetNumberThenId, UpdateAttributeLoop, GetAuthToken, AfterGetAuthToken, @@ -117,17 +115,19 @@ public UpdateTicketDialog( protected async Task UpdateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) { var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + + if (string.IsNullOrEmpty(state.TicketDescription) && state.UrgencyLevel == UrgencyLevel.None) + { + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.TicketNoUpdate)); + return await sc.NextAsync(); + } + var management = ServiceManager.CreateManagement(Settings, state.Token); var result = await management.UpdateTicket(state.Id, state.TicketDescription, state.UrgencyLevel); if (!result.Success) { - var errorReplacements = new StringDictionary - { - { "Error", result.ErrorMessage } - }; - await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ServiceFailed, errorReplacements)); - return await sc.CancelAllDialogsAsync(); + return await SendServiceErrorAndCancel(sc, result); } var card = new Card() diff --git a/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs b/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs index 668c760ea9..9762642d1d 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs @@ -14,5 +14,7 @@ public enum AttributeType Urgency, [EnumLocalizedDescription("AttributeState", typeof(SharedStrings))] State, + [EnumLocalizedDescription("AttributeNumber", typeof(SharedStrings))] + Number, } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/TicketResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/TicketResponse.cs index 2a35f7135f..e286b4a8e7 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/TicketResponse.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/TicketResponse.cs @@ -23,6 +23,8 @@ public class TicketResponse public string urgency { get; set; } + public string number { get; set; } + public class UserInfo { public string value { get; set; } diff --git a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs index 0cef2a2f4c..e23a728433 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs @@ -22,7 +22,9 @@ public SkillState() public int PageIndex { get; set; } // used when from ShowKnowledge to CreateTicket - public bool SkipDisplayExisting { get; set; } + public bool DisplayExisting { get; set; } + + public Ticket TicketTarget { get; set; } public string Id { get; set; } @@ -36,6 +38,9 @@ public SkillState() public TicketState TicketState { get; set; } + // INC[0-9]{7} + public string TicketNumber { get; set; } + // from OnInterruptDialogAsync public GeneralLuis.Intent GeneralIntent { get; set; } @@ -69,6 +74,11 @@ public void DigestLuisResult(ITSMLuis luis, ITSMLuis.Intent topIntent) TicketState = Enum.Parse(luis.Entities.TicketState[0][0], true); } + if (luis.Entities.TicketNumber != null) + { + TicketNumber = luis.Entities.TicketNumber[0].ToUpper(); + } + // TODO some special digestions if (topIntent == ITSMLuis.Intent.TicketUpdate) { @@ -84,7 +94,7 @@ public void DigestLuisResult(ITSMLuis luis, ITSMLuis.Intent topIntent) } else if (topIntent == ITSMLuis.Intent.TicketCreate) { - SkipDisplayExisting = false; + DisplayExisting = true; } else if (topIntent == ITSMLuis.Intent.TicketShow) { @@ -100,6 +110,7 @@ public void ClearLuisResult() UrgencyLevel = UrgencyLevel.None; AttributeType = AttributeType.None; TicketState = TicketState.None; + TicketNumber = null; } } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/Ticket.cs b/skills/src/csharp/experimental/itsmskill/Models/Ticket.cs index f9d22498b5..e7cd6d88f5 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/Ticket.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/Ticket.cs @@ -18,5 +18,7 @@ public class Ticket public DateTime OpenedTime { get; set; } public string ResolvedReason { get; set; } + + public string Number { get; set; } } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/TicketCard.cs b/skills/src/csharp/experimental/itsmskill/Models/TicketCard.cs index dd836c553b..07e5c54366 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/TicketCard.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/TicketCard.cs @@ -19,5 +19,7 @@ public class TicketCard : ICardData public string ResolvedReason { get; set; } public string Speak { get; set; } + + public string Number { get; set; } } } diff --git a/skills/src/csharp/experimental/itsmskill/Prompts/TicketNumberPrompt.cs b/skills/src/csharp/experimental/itsmskill/Prompts/TicketNumberPrompt.cs new file mode 100644 index 0000000000..86e525a167 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Prompts/TicketNumberPrompt.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using ITSMSkill.Models; +using Luis; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text.Number; +using static Microsoft.Recognizers.Text.Culture; + +namespace ITSMSkill.Prompts +{ + public class TicketNumberPrompt : Prompt + { + public TicketNumberPrompt(string dialogId, PromptValidator validator = null, string defaultLocale = null) +: base(dialogId, validator) + { + DefaultLocale = defaultLocale; + } + + public string DefaultLocale { get; set; } + + protected override async Task OnPromptAsync(ITurnContext turnContext, IDictionary state, PromptOptions options, bool isRetry, CancellationToken cancellationToken = default(CancellationToken)) + { + if (turnContext == null) + { + throw new ArgumentNullException(nameof(turnContext)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (isRetry && options.RetryPrompt != null) + { + await turnContext.SendActivityAsync(options.RetryPrompt, cancellationToken).ConfigureAwait(false); + } + else if (options.Prompt != null) + { + await turnContext.SendActivityAsync(options.Prompt, cancellationToken).ConfigureAwait(false); + } + } + + protected override async Task> OnRecognizeAsync(ITurnContext turnContext, IDictionary state, PromptOptions options, CancellationToken cancellationToken = default(CancellationToken)) + { + if (turnContext == null) + { + throw new ArgumentNullException(nameof(turnContext)); + } + + var result = new PromptRecognizerResult(); + + if (turnContext.Activity.Type == ActivityTypes.Message) + { + var message = turnContext.Activity.AsMessageActivity().Text.ToUpper(); + + var regex = new Regex("INC[0-9]{7}"); + if (regex.IsMatch(message)) + { + result.Succeeded = true; + result.Value = message; + } + + if (!result.Succeeded) + { + var culture = turnContext.Activity.Locale ?? DefaultLocale ?? English; + var results = NumberRecognizer.RecognizeNumber(message, culture); + if (results.Count > 0) + { + var text = results[0].Resolution["value"].ToString(); + if (int.TryParse(text, NumberStyles.Any, new CultureInfo(culture), out var value)) + { + if (value >= 1 && value <= 9999999) + { + result.Succeeded = true; + result.Value = $"INC{value:D7}"; + } + } + } + } + } + + return await Task.FromResult(result); + } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs index 2baf09a15a..d89353ecb6 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs @@ -28,6 +28,7 @@ public class SharedResponses : IResponseIdCollection public const string InputState = "InputState"; public const string ConfirmId = "ConfirmId"; public const string InputId = "InputId"; + public const string InputTicketNumber = "InputTicketNumber"; public const string ServiceFailed = "ServiceFailed"; } } \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json index 198b4f2732..93d50cf7b0 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json @@ -235,6 +235,15 @@ ], "inputHint": "expectingInput" }, + "InputTicketNumber": { + "replies": [ + { + "text": "Please input the ticket number:", + "speak": "Please input the ticket number:" + } + ], + "inputHint": "expectingInput" + }, "ServiceFailed": { "replies": [ { diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs index 93377f64d1..d8af0b0c49 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs @@ -78,6 +78,15 @@ public static string AttributeId { } } + /// + /// Looks up a localized string similar to number. + /// + public static string AttributeNumber { + get { + return ResourceManager.GetString("AttributeNumber", resourceCulture); + } + } + /// /// Looks up a localized string similar to state. /// @@ -123,6 +132,15 @@ public static string OpenedAt { } } + /// + /// Looks up a localized string similar to Number: . + /// + public static string TicketNumber { + get { + return ResourceManager.GetString("TicketNumber", resourceCulture); + } + } + /// /// Looks up a localized string similar to State: . /// diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx index c255ad16ca..e968cd5784 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx @@ -174,4 +174,10 @@ state + + Number: + + + number + \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs index ec0fc62e99..d7d670bdae 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs @@ -17,6 +17,7 @@ public class TicketResponses : IResponseIdCollection public const string ConfirmUpdateAttribute = "ConfirmUpdateAttribute"; public const string UpdateAttribute = "UpdateAttribute"; public const string TicketUpdated = "TicketUpdated"; + public const string TicketNoUpdate = "TicketNoUpdate"; public const string ShowConstraintNone = "ShowConstraintNone"; public const string ShowConstraints = "ShowConstraints"; public const string ShowUpdateNone = "ShowUpdateNone"; @@ -25,6 +26,9 @@ public class TicketResponses : IResponseIdCollection public const string TicketShow = "TicketShow"; public const string TicketEnd = "TicketEnd"; public const string TicketShowNone = "TicketShowNone"; + public const string TicketDuplicateNumber = "TicketDuplicateNumber"; + public const string TicketTarget = "TicketTarget"; + public const string TicketAlreadyClosed = "TicketAlreadyClosed"; public const string TicketClosed = "TicketClosed"; } } \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json index 02554b41fb..52f7d47891 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json @@ -46,6 +46,15 @@ ], "inputHint": "acceptingInput" }, + "TicketNoUpdate": { + "replies": [ + { + "text": "You didn't set anything to update. Let me know when you need my help.", + "speak": "You didn't set anything to update. Let me know when you need my help." + } + ], + "inputHint": "acceptingInput" + }, "ShowConstraintNone": { "replies": [ { @@ -84,7 +93,7 @@ }, "ShowAttribute": { // same with SharedStrings - "suggestedActions": [ "id", "description", "urgency", "state", "no" ], + "suggestedActions": [ "number", "description", "urgency", "state", "no" ], "replies": [ { "text": "What attribute do you want to set as search constraint? Or \"No\" for no more attribute.", @@ -120,6 +129,33 @@ ], "inputHint": "acceptingInput" }, + "TicketDuplicateNumber": { + "replies": [ + { + "text": "Service reports duplicate ticket numbers. Please contact service manager for help.", + "speak": "Service reports duplicate ticket numbers. Please contact service manager for help." + } + ], + "inputHint": "acceptingInput" + }, + "TicketTarget": { + "replies": [ + { + "text": "Here is your current ticket:", + "speak": "Here is your current ticket:" + } + ], + "inputHint": "acceptingInput" + }, + "TicketAlreadyClosed": { + "replies": [ + { + "text": "Your ticket has already been closed. Let me know when you need my help.", + "speak": "Your ticket has already been closed. Let me know when you need my help." + } + ], + "inputHint": "acceptingInput" + }, "TicketClosed": { "replies": [ { diff --git a/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs b/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs index e9c63ec131..40c8c28a81 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs @@ -9,7 +9,7 @@ public interface IITServiceManagement Task CreateTicket(string description, UrgencyLevel urgency); // like description & in urgencies & equal id & in states - Task SearchTicket(int pageIndex, string description = null, List urgencies = null, string id = null, List states = null); + Task SearchTicket(int pageIndex, string description = null, List urgencies = null, string id = null, List states = null, string number = null); Task UpdateTicket(string id, string description = null, UrgencyLevel urgency = UrgencyLevel.None); diff --git a/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs index aae29a3a15..55564674d8 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs @@ -43,12 +43,16 @@ public class _Entities public string[][] UrgencyLevel; + // Regex entities + public string[] TicketNumber; + // Instance public class _Instance { public InstanceData[] AttributeType; public InstanceData[] CloseReason; public InstanceData[] TicketDescription; + public InstanceData[] TicketNumber; public InstanceData[] TicketState; public InstanceData[] UrgencyLevel; } diff --git a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs index a9c1c6aa0c..8433f9fa05 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs @@ -89,7 +89,7 @@ public async Task CreateTicket(string description, UrgencyLevel u } } - public async Task SearchTicket(int pageIndex, string description = null, List urgencies = null, string id = null, List states = null) + public async Task SearchTicket(int pageIndex, string description = null, List urgencies = null, string id = null, List states = null, string number = null) { try { @@ -123,6 +123,11 @@ public async Task SearchTicket(int pageIndex, string description sysparmQuery.Add($"stateIN{string.Join(',', states.Select(state => TicketStateToString[state]))}"); } + if (!string.IsNullOrEmpty(number)) + { + sysparmQuery.Add($"number={number}"); + } + request.AddParameter("sysparm_query", string.Join('^', sysparmQuery)); request.AddParameter("sysparm_limit", limitSize); @@ -251,7 +256,8 @@ private Ticket ConvertTicket(TicketResponse ticketResponse) Description = ticketResponse.short_description, Urgency = StringToUrgency[ticketResponse.urgency], State = StringToTicketState[ticketResponse.state], - OpenedTime = DateTime.Parse(ticketResponse.opened_at) + OpenedTime = DateTime.Parse(ticketResponse.opened_at), + Number = ticketResponse.number, }; if (!string.IsNullOrEmpty(ticketResponse.close_code)) From 64d4cba085d0dd2afc37df01701505249667abda Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Mon, 26 Aug 2019 17:25:42 +0800 Subject: [PATCH 11/15] [ITSM] add url and number to knowledge base --- .../experimental/itsmskill/Content/Knowledge.1.0.json | 9 ++++++++- .../csharp/experimental/itsmskill/Content/Knowledge.json | 9 ++++++++- .../experimental/itsmskill/Dialogs/SkillDialogBase.cs | 5 ++++- .../csharp/experimental/itsmskill/Models/Knowledge.cs | 4 ++++ .../experimental/itsmskill/Models/KnowledgeCard.cs | 6 ++++++ .../itsmskill/Models/ServiceNow/KnowledgeResponse.cs | 2 ++ .../itsmskill/Responses/Shared/SharedStrings.Designer.cs | 9 +++++++++ .../itsmskill/Responses/Shared/SharedStrings.resx | 3 +++ .../itsmskill/Services/ServiceNow/Management.cs | 6 +++++- 9 files changed, 49 insertions(+), 4 deletions(-) diff --git a/skills/src/csharp/experimental/itsmskill/Content/Knowledge.1.0.json b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.1.0.json index 62835c0d8a..fae9cb6cf7 100644 --- a/skills/src/csharp/experimental/itsmskill/Content/Knowledge.1.0.json +++ b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.1.0.json @@ -35,7 +35,7 @@ "type": "TextBlock", "size": "Small", "color": "Default", - "text": "{Id}" + "text": "{Number}" } ] }, @@ -46,6 +46,13 @@ "maxLines": 5 } ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "{UrlTitle}", + "url": "{UrlLink}" + } + ], "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "version": "1.0", "speak": "{Speak}" diff --git a/skills/src/csharp/experimental/itsmskill/Content/Knowledge.json b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.json index 4559df1887..669e7831a2 100644 --- a/skills/src/csharp/experimental/itsmskill/Content/Knowledge.json +++ b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.json @@ -51,7 +51,7 @@ "type": "TextBlock", "size": "Small", "color": "Light", - "text": "{Id}" + "text": "{Number}" } ] }, @@ -62,6 +62,13 @@ "maxLines": 5 } ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "{UrlTitle}", + "url": "{UrlLink}" + } + ], "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "version": "1.0", "speak": "{Speak}" diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs index 1141ce5637..e6e2e1aea7 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs @@ -868,7 +868,10 @@ protected KnowledgeCard ConvertKnowledge(Knowledge knowledge) Title = knowledge.Title, UpdatedTime = $"{SharedStrings.UpdatedAt}{knowledge.UpdatedTime.ToString()}", Content = knowledge.Content, - Speak = knowledge.Title + Speak = knowledge.Title, + Number = $"{SharedStrings.TicketNumber}{knowledge.Number}", + UrlTitle = SharedStrings.OpenKnowledge, + UrlLink = knowledge.Url, }; return card; } diff --git a/skills/src/csharp/experimental/itsmskill/Models/Knowledge.cs b/skills/src/csharp/experimental/itsmskill/Models/Knowledge.cs index 822edf43f5..8fc7814b8c 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/Knowledge.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/Knowledge.cs @@ -14,5 +14,9 @@ public class Knowledge public DateTime UpdatedTime { get; set; } public string Content { get; set; } + + public string Number { get; set; } + + public string Url { get; set; } } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/KnowledgeCard.cs b/skills/src/csharp/experimental/itsmskill/Models/KnowledgeCard.cs index 26045e46af..0cbdfe9d04 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/KnowledgeCard.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/KnowledgeCard.cs @@ -13,5 +13,11 @@ public class KnowledgeCard : ICardData public string Content { get; set; } public string Speak { get; set; } + + public string Number { get; set; } + + public string UrlTitle { get; set; } + + public string UrlLink { get; set; } } } diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/KnowledgeResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/KnowledgeResponse.cs index 1f095d0394..88f1d18728 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/KnowledgeResponse.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/KnowledgeResponse.cs @@ -16,5 +16,7 @@ public class KnowledgeResponse public string text { get; set; } public string wiki { get; set; } + + public string number { get; set; } } } diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs index d8af0b0c49..7c1110184f 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs @@ -132,6 +132,15 @@ public static string OpenedAt { } } + /// + /// Looks up a localized string similar to Open Article. + /// + public static string OpenKnowledge { + get { + return ResourceManager.GetString("OpenKnowledge", resourceCulture); + } + } + /// /// Looks up a localized string similar to Number: . /// diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx index e968cd5784..5b798c2c21 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx @@ -180,4 +180,7 @@ number + + Open Article + \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs index 8433f9fa05..7975ff9e46 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs @@ -24,6 +24,7 @@ public class Management : IITServiceManagement private readonly string getUserIdResource; private readonly string token; private readonly int limitSize; + private readonly string knowledgeUrl; static Management() { @@ -54,6 +55,7 @@ public Management(string url, string token, int limitSize, string getUserIdResou this.getUserIdResource = getUserIdResource; this.token = token; this.limitSize = limitSize; + this.knowledgeUrl = $"{url}/kb_view.do?sysparm_article={{0}}"; } public async Task CreateTicket(string description, UrgencyLevel urgency) @@ -285,7 +287,9 @@ private Knowledge ConvertKnowledge(KnowledgeResponse knowledgeResponse) { Id = knowledgeResponse.sys_id, Title = knowledgeResponse.short_description, - UpdatedTime = DateTime.Parse(knowledgeResponse.sys_updated_on) + UpdatedTime = DateTime.Parse(knowledgeResponse.sys_updated_on), + Number = knowledgeResponse.number, + Url = string.Format(knowledgeUrl, knowledgeResponse.number), }; if (!string.IsNullOrEmpty(knowledgeResponse.text)) { From 27f78723b711270b340eca61e769bf3e11804e02 Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Fri, 30 Aug 2019 14:38:53 +0800 Subject: [PATCH 12/15] [ITSM] separate SimpleWhitelistAuthenticationProvider, add License statement --- .../csharp/experimental/itsmskill/ITSMSkill.csproj | 4 ++-- .../experimental/itsmskill/Models/AttributeType.cs | 5 ++++- .../experimental/itsmskill/Models/Knowledge.cs | 5 ++++- .../experimental/itsmskill/Models/KnowledgeCard.cs | 5 ++++- .../itsmskill/Models/KnowledgesResult.cs | 5 ++++- .../experimental/itsmskill/Models/ResultBase.cs | 5 ++++- .../Models/ServiceNow/CreateTicketRequest.cs | 5 ++++- .../Models/ServiceNow/GetUserIdResponse.cs | 6 ++---- .../Models/ServiceNow/KnowledgeResponse.cs | 6 ++---- .../Models/ServiceNow/MultiKnowledgesResponse.cs | 6 +++--- .../Models/ServiceNow/MultiTicketsResponse.cs | 6 +++--- .../Models/ServiceNow/SingleTicketResponse.cs | 6 ++---- .../itsmskill/Models/ServiceNow/TicketResponse.cs | 6 ++---- .../csharp/experimental/itsmskill/Models/Ticket.cs | 5 ++++- .../experimental/itsmskill/Models/TicketCard.cs | 5 ++++- .../experimental/itsmskill/Models/TicketState.cs | 5 ++++- .../experimental/itsmskill/Models/TicketsResult.cs | 5 ++++- .../experimental/itsmskill/Models/UrgencyLevel.cs | 5 ++++- .../itsmskill/Services/IITServiceManagement.cs | 5 ++++- .../itsmskill/Services/IServiceManager.cs | 5 ++++- .../itsmskill/Services/ServiceManager.cs | 5 ++++- .../itsmskill/Services/ServiceNow/Management.cs | 5 ++++- .../src/csharp/experimental/itsmskill/Startup.cs | 7 +------ .../itsmskill/Utilities/EnumToLocalizedString.cs | 5 ++++- .../SimpleWhitelistAuthenticationProvider.cs | 14 ++++++++++++++ 25 files changed, 95 insertions(+), 46 deletions(-) create mode 100644 skills/src/csharp/experimental/itsmskill/Utilities/SimpleWhitelistAuthenticationProvider.cs diff --git a/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj index 398e00909d..763a07da26 100644 --- a/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj +++ b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj @@ -55,8 +55,8 @@ - - + + diff --git a/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs b/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs index 9762642d1d..194fd24a5b 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs @@ -1,4 +1,7 @@ -using ITSMSkill.Responses.Shared; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using ITSMSkill.Responses.Shared; using ITSMSkill.Utilities; namespace ITSMSkill.Models diff --git a/skills/src/csharp/experimental/itsmskill/Models/Knowledge.cs b/skills/src/csharp/experimental/itsmskill/Models/Knowledge.cs index 8fc7814b8c..83ee78aa76 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/Knowledge.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/Knowledge.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/skills/src/csharp/experimental/itsmskill/Models/KnowledgeCard.cs b/skills/src/csharp/experimental/itsmskill/Models/KnowledgeCard.cs index 0cbdfe9d04..1a5a8fec23 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/KnowledgeCard.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/KnowledgeCard.cs @@ -1,4 +1,7 @@ -using Microsoft.Bot.Builder.Solutions.Responses; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder.Solutions.Responses; namespace ITSMSkill.Models { diff --git a/skills/src/csharp/experimental/itsmskill/Models/KnowledgesResult.cs b/skills/src/csharp/experimental/itsmskill/Models/KnowledgesResult.cs index b1003118a8..3f09cb8dda 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/KnowledgesResult.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/KnowledgesResult.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/skills/src/csharp/experimental/itsmskill/Models/ResultBase.cs b/skills/src/csharp/experimental/itsmskill/Models/ResultBase.cs index 023925e326..9edced93d7 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ResultBase.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ResultBase.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs index 19648646cd..48bc878ccb 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs @@ -1,4 +1,7 @@ -namespace ITSMSkill.Models.ServiceNow +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace ITSMSkill.Models.ServiceNow { public class CreateTicketRequest { diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/GetUserIdResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/GetUserIdResponse.cs index c499464b0e..790b0ec768 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/GetUserIdResponse.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/GetUserIdResponse.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. namespace ITSMSkill.Models.ServiceNow { diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/KnowledgeResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/KnowledgeResponse.cs index 88f1d18728..92d3875866 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/KnowledgeResponse.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/KnowledgeResponse.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. namespace ITSMSkill.Models.ServiceNow { diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiKnowledgesResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiKnowledgesResponse.cs index b64bc65307..9fa1309885 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiKnowledgesResponse.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiKnowledgesResponse.cs @@ -1,7 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace ITSMSkill.Models.ServiceNow { diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiTicketsResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiTicketsResponse.cs index 4d64e4518e..c0bb5a3a6c 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiTicketsResponse.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiTicketsResponse.cs @@ -1,7 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace ITSMSkill.Models.ServiceNow { diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SingleTicketResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SingleTicketResponse.cs index 4a857dfd9c..e36b1175ed 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SingleTicketResponse.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SingleTicketResponse.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. namespace ITSMSkill.Models.ServiceNow { diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/TicketResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/TicketResponse.cs index e286b4a8e7..721a2ae93b 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/TicketResponse.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/TicketResponse.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. namespace ITSMSkill.Models.ServiceNow { diff --git a/skills/src/csharp/experimental/itsmskill/Models/Ticket.cs b/skills/src/csharp/experimental/itsmskill/Models/Ticket.cs index e7cd6d88f5..e1c9efb5f0 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/Ticket.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/Ticket.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/skills/src/csharp/experimental/itsmskill/Models/TicketCard.cs b/skills/src/csharp/experimental/itsmskill/Models/TicketCard.cs index 07e5c54366..dab52a7d54 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/TicketCard.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/TicketCard.cs @@ -1,4 +1,7 @@ -using Microsoft.Bot.Builder.Solutions.Responses; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder.Solutions.Responses; namespace ITSMSkill.Models { diff --git a/skills/src/csharp/experimental/itsmskill/Models/TicketState.cs b/skills/src/csharp/experimental/itsmskill/Models/TicketState.cs index b47e6e8ee1..1ca0ed3c45 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/TicketState.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/TicketState.cs @@ -1,4 +1,7 @@ -using ITSMSkill.Responses.Shared; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using ITSMSkill.Responses.Shared; using ITSMSkill.Utilities; namespace ITSMSkill.Models diff --git a/skills/src/csharp/experimental/itsmskill/Models/TicketsResult.cs b/skills/src/csharp/experimental/itsmskill/Models/TicketsResult.cs index 3d5fdbe623..91611d8ad5 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/TicketsResult.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/TicketsResult.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/skills/src/csharp/experimental/itsmskill/Models/UrgencyLevel.cs b/skills/src/csharp/experimental/itsmskill/Models/UrgencyLevel.cs index 3c053d178f..a1647bd3fc 100644 --- a/skills/src/csharp/experimental/itsmskill/Models/UrgencyLevel.cs +++ b/skills/src/csharp/experimental/itsmskill/Models/UrgencyLevel.cs @@ -1,4 +1,7 @@ -using ITSMSkill.Responses.Shared; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using ITSMSkill.Responses.Shared; using ITSMSkill.Utilities; namespace ITSMSkill.Models diff --git a/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs b/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs index 40c8c28a81..b75b852a02 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; using System.Threading.Tasks; using ITSMSkill.Models; diff --git a/skills/src/csharp/experimental/itsmskill/Services/IServiceManager.cs b/skills/src/csharp/experimental/itsmskill/Services/IServiceManager.cs index ac0969c062..c696fe4a0e 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/IServiceManager.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/IServiceManager.cs @@ -1,4 +1,7 @@ -using Microsoft.Bot.Schema; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Schema; namespace ITSMSkill.Services { diff --git a/skills/src/csharp/experimental/itsmskill/Services/ServiceManager.cs b/skills/src/csharp/experimental/itsmskill/Services/ServiceManager.cs index daa4f62dd4..a6063ed17f 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ServiceManager.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceManager.cs @@ -1,4 +1,7 @@ -using Microsoft.Bot.Schema; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Schema; namespace ITSMSkill.Services { diff --git a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs index 7975ff9e46..2d155ef8fc 100644 --- a/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/skills/src/csharp/experimental/itsmskill/Startup.cs b/skills/src/csharp/experimental/itsmskill/Startup.cs index c6437d7795..ee103eda49 100644 --- a/skills/src/csharp/experimental/itsmskill/Startup.cs +++ b/skills/src/csharp/experimental/itsmskill/Startup.cs @@ -11,6 +11,7 @@ using ITSMSkill.Responses.Shared; using ITSMSkill.Responses.Ticket; using ITSMSkill.Services; +using ITSMSkill.Utilities; using Microsoft.ApplicationInsights; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -139,11 +140,5 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) .UseWebSockets() .UseMvc(); } - - public class SimpleWhitelistAuthenticationProvider : IWhitelistAuthenticationProvider - { - // set VA appid here - public List AppsWhitelist => new List { }; - } } } \ No newline at end of file diff --git a/skills/src/csharp/experimental/itsmskill/Utilities/EnumToLocalizedString.cs b/skills/src/csharp/experimental/itsmskill/Utilities/EnumToLocalizedString.cs index a972cdd51f..088e3d78aa 100644 --- a/skills/src/csharp/experimental/itsmskill/Utilities/EnumToLocalizedString.cs +++ b/skills/src/csharp/experimental/itsmskill/Utilities/EnumToLocalizedString.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; diff --git a/skills/src/csharp/experimental/itsmskill/Utilities/SimpleWhitelistAuthenticationProvider.cs b/skills/src/csharp/experimental/itsmskill/Utilities/SimpleWhitelistAuthenticationProvider.cs new file mode 100644 index 0000000000..68c55a3301 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Utilities/SimpleWhitelistAuthenticationProvider.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Bot.Builder.Skills.Auth; + +namespace ITSMSkill.Utilities +{ + public class SimpleWhitelistAuthenticationProvider : IWhitelistAuthenticationProvider + { + // set VA appid here + public HashSet AppsWhitelist => new HashSet { }; + } +} From c5a169a2558d037da78b6e5c9cdeecc90246b648 Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Tue, 3 Sep 2019 11:48:48 +0800 Subject: [PATCH 13/15] [ITSM] update json and doc --- docs/_docs/reference/skills/experimental.md | 24 ++++++++++++++----- .../Responses/Knowledge/KnowledgeResponses.cs | 1 - .../Knowledge/KnowledgeResponses.json | 12 +++++----- .../Responses/Main/MainResponses.json | 4 ++-- .../Responses/Shared/SharedResponses.json | 16 ++++++------- .../Responses/Ticket/TicketResponses.json | 12 +++++----- .../csharp/experimental/itsmskill/readme.md | 2 +- 7 files changed, 41 insertions(+), 30 deletions(-) diff --git a/docs/_docs/reference/skills/experimental.md b/docs/_docs/reference/skills/experimental.md index 8d3e13be10..cc29715e86 100644 --- a/docs/_docs/reference/skills/experimental.md +++ b/docs/_docs/reference/skills/experimental.md @@ -37,21 +37,33 @@ The Weather skill provides a basic Skill that integrates with [AccuWeather](http The Music skill integrates with [Spotify](https://developer.spotify.com/documentation/web-api/libraries/) to look up playlists and artists and open via the Spotify app. Provide credentials after you [create a Spotify client](https://developer.spotify.com/dashboard/) in the appsettings to configure the skill. -## IT Service Managerment Skill +## IT Service Management Skill -The [IT Service Managerment skill](https://github.com/microsoft/AI/tree/next/skills/src/csharp/experimental/itsmskill) provides a basic skill that provides ticket and knowledge base related capabilities and supports SerivceNow. +The [IT Service Management skill](https://github.com/microsoft/AI/tree/next/skills/src/csharp/experimental/itsmskill) provides a basic skill that provides ticket and knowledge base related capabilities and supports SerivceNow. To test this skill, one should setup the following: -* Create a ServiceNow instance in [Developers](https://developer.servicenow.com/app.do#!/instance) and update the serviceNowUrl of appsettings.json -* Set up a scripted REST API for current user's sys_id following [this question](https://community.servicenow.com/community?id=community_question&sys_id=52efcb88db1ddb084816f3231f9619c7) and update the serviceNowGetUserId of appsetting.json - - Please raise an issue if simpler way is found +* Create a ServiceNow instance in [Developers](https://developer.servicenow.com/app.do#!/instance) and update the serviceNowUrl of appsettings.json: `"serviceNowUrl": "https://YOUR_INSTANCE_NAME.service-now.com"` +* Create a [scripted REST API](https://docs.servicenow.com/bundle/geneva-servicenow-platform/page/integrate/custom_web_services/task/t_CreateAScriptedRESTService.html) to get current user's sys_id and please raise an issue if simpler way is found + - In System Web Services/Scripted REST APIs, click New to create an API + - In API's Resources, click New to add a resource + - In the resource, select GET for HTTP method and input `(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) { return gs.getUserID(); })(request, response);` in Script + - Update the serviceNowGetUserId of appsetting.json: `"serviceNowGetUserId": "YOUR_API_NAMESPACE/YOUR_API_ID"` * Set up endpoint by [this document](https://docs.servicenow.com/bundle/london-platform-administration/page/administer/security/task/t_CreateEndpointforExternalClients.html#t_CreateEndpointforExternalClients) for Client id and Client secret to be used in the following OAuth Connection - Redirect URL is https://token.botframework.com/.auth/web/redirect * Add an OAuth Connection in the Settings of Web App Bot named 'ServiceNow' with Service Provider 'Generic Oauth 2' - Authorization URL as https://instance.service-now.com/oauth_auth.do - Token URL, Refresh URL as https://instance.service-now.com/oauth_token.do -* If one wants to use it in VA, add VA's appId to AppsWhitelist in the Startup.cs + - No Scopes are needed + - Click Test Connection to verify + +To test this skill in VA, one should setup the following: + +* Add https://botbuilder.myget.org/F/aitemplates/api/v3/index.json as NuGet package source +* Update VA's Microsoft.Bot.Builder.Solutions and Microsoft.Bot.Builder.Skills to 4.6.0-daily27 as this skill +* Add VA's appId to AppsWhitelist of SimpleWhitelistAuthenticationProvider under Utilities +* Add OAuth Connection as skill +* The remaining steps are same as normal skills ## Experimental Skill Deployment diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs index 6f88839d7b..500b2ba3fe 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs +++ b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs @@ -14,7 +14,6 @@ public class KnowledgeResponses : IResponseIdCollection // Generated accessors public const string IfFindWanted = "IfFindWanted"; public const string IfCreateTicket = "IfCreateTicket"; - public const string KnowledgeShow = "KnowledgeShow"; public const string KnowledgeEnd = "KnowledgeEnd"; public const string KnowledgeShowNone = "KnowledgeShowNone"; } diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.json index 591329c7f9..276d553092 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.json @@ -2,8 +2,8 @@ "IfFindWanted": { "replies": [ { - "text": "Page {Page}. Do you find what you want? \"Go forward/previously\" to navigate.", - "speak": "Page {Page}. Do you find what you want? \"Go forward/previously\" to navigate." + "text": "Page {Page}. Do you find what you want? \"Go forward/previous\" to navigate.", + "speak": "Page {Page}. Do you find what you want? \"Go forward/previous\" to navigate." } ], "inputHint": "expectingInput" @@ -20,8 +20,8 @@ "KnowledgeEnd": { "replies": [ { - "text": "Page {Page} does not contain anything, try \"Go previously\".", - "speak": "Page {Page} does not contain anything, try \"Go previously\"." + "text": "Page {Page} does not contain anything, try \"Go previous\".", + "speak": "Page {Page} does not contain anything, try \"Go previous\"." } ], "inputHint": "acceptingInput" @@ -29,8 +29,8 @@ "KnowledgeShowNone": { "replies": [ { - "text": "I'm sorry I can't find any. Let me know when you need my help.", - "speak": "I'm sorry I can't find any. Let me know when you need my help." + "text": "I'm sorry, I can't find any articles. Let me know if I can help in another way.", + "speak": "I'm sorry, I can't find any articles. Let me know if I can help in another way." } ], "inputHint": "acceptingInput" diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.json index 9c4f2f4e7f..7708c37652 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.json @@ -2,8 +2,8 @@ "WelcomeMessage": { "replies": [ { - "text": "Welcome to IT Service Managerment skill! You can view, create or resolve tickets here.", - "speak": "Welcome to IT Service Managerment skill! You can view, create or resolve tickets here." + "text": "Welcome to the IT Service Management skill! You can view, create or resolve tickets here.", + "speak": "Welcome to the IT Service Management skill! You can view, create or resolve tickets here." } ], "suggestedActions": [], diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json index 93d50cf7b0..16297db4a9 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json @@ -110,8 +110,8 @@ "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": "Let me know if you need my help with anything else.", + "speak": "Let me know if you need my help with anything else." }, { "text": "I'm here if you need me.", @@ -157,8 +157,8 @@ "InputDescription": { "replies": [ { - "text": "Please input the description:", - "speak": "Please input the description:" + "text": "Please provide the description:", + "speak": "Please provide the description:" } ], "inputHint": "expectingInput" @@ -238,8 +238,8 @@ "InputTicketNumber": { "replies": [ { - "text": "Please input the ticket number:", - "speak": "Please input the ticket number:" + "text": "Please provide the ticket number:", + "speak": "Please provide the ticket number:" } ], "inputHint": "expectingInput" @@ -247,8 +247,8 @@ "ServiceFailed": { "replies": [ { - "text": "Sorry, IT service failed due to {Error}.", - "speak": "Sorry, IT service failed due to {Error}." + "text": "Sorry, connecting to the IT Service Management service failed due to {Error}.", + "speak": "Sorry, connecting to the IT Service Management service failed due to {Error}." } ], "inputHint": "acceptingInput" diff --git a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json index 52f7d47891..4e957c03d5 100644 --- a/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json @@ -2,8 +2,8 @@ "IfExistingSolve": { "replies": [ { - "text": "Page {Page}. Does one of these solve your problem? \"Go forward/previously\" to navigate.", - "speak": "Page {Page}. Does one of these solve your problem? \"Go forward/previously\" to navigate." + "text": "Page {Page}. Do any of these solve your problem? Go forward/previous to navigate", + "speak": "Page {Page}. Do any of these solve your problem? Go forward/previous to navigate" } ], "inputHint": "expectingInput" @@ -31,8 +31,8 @@ "suggestedActions": [ "description", "urgency", "no" ], "replies": [ { - "text": "What attribute do you want to set for updating? Or \"No\" for no more attribute.", - "speak": "What attribute do you want to set for updating? Or \"No\" for no more attribute." + "text": "Which attribute do you want to update?", + "speak": "Which attribute do you want to update?" } ], "inputHint": "expectingInput" @@ -49,8 +49,8 @@ "TicketNoUpdate": { "replies": [ { - "text": "You didn't set anything to update. Let me know when you need my help.", - "speak": "You didn't set anything to update. Let me know when you need my help." + "text": "You didn't provide any changes, let me know when you need my help.", + "speak": "You didn't provide any changes, let me know when you need my help." } ], "inputHint": "acceptingInput" diff --git a/skills/src/csharp/experimental/itsmskill/readme.md b/skills/src/csharp/experimental/itsmskill/readme.md index 63a5b76a4a..5535ee2405 100644 --- a/skills/src/csharp/experimental/itsmskill/readme.md +++ b/skills/src/csharp/experimental/itsmskill/readme.md @@ -1,3 +1,3 @@ -# IT Service Managerment Experimental Skill +# IT Service Management Experimental Skill See the Experimental Skill [documentation page](/docs/reference/skills/experimental.md) for information on how to deploy and test this Skill. From c4415200e6aa9031b4840d5f2bf1bbd7b9ca7ba5 Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Tue, 3 Sep 2019 18:02:52 +0800 Subject: [PATCH 14/15] [ITSM] update GUID --- .../experimental/itsmskill/Deployment/Resources/template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/template.json b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/template.json index 5eb8a2cab3..c0c007cebe 100644 --- a/skills/src/csharp/experimental/itsmskill/Deployment/Resources/template.json +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/template.json @@ -89,7 +89,7 @@ "resources": [ { "apiVersion": "2018-02-01", - "name": "99ea37e6-a3e6-4102-a249-71c880607386", + "name": "5a40ff56-5dad-475a-9aea-f7b3fa0cf17e", "type": "Microsoft.Resources/deployments", "properties": { "mode": "Incremental", From c48d72df87a785933e55662299c9e18e20a0ae66 Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Wed, 4 Sep 2019 10:56:21 +0800 Subject: [PATCH 15/15] [ITSM] change to handoff as others --- .../src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs index 203540d5f7..209b3009c4 100644 --- a/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs @@ -158,7 +158,7 @@ public MainDialog( if (dc.Context.Adapter is IRemoteUserTokenProvider || Channel.GetChannelId(dc.Context) != Channels.Msteams) { var response = dc.Context.Activity.CreateReply(); - response.Type = ActivityTypes.EndOfConversation; + response.Type = ActivityTypes.Handoff; await dc.Context.SendActivityAsync(response); } @@ -178,7 +178,7 @@ public MainDialog( if (result.Status != DialogTurnStatus.Waiting) { var response = dc.Context.Activity.CreateReply(); - response.Type = ActivityTypes.EndOfConversation; + response.Type = ActivityTypes.Handoff; await dc.Context.SendActivityAsync(response); }