diff --git a/docs/_docs/reference/skills/experimental.md b/docs/_docs/reference/skills/experimental.md index a8aad4e70a..cc29715e86 100644 --- a/docs/_docs/reference/skills/experimental.md +++ b/docs/_docs/reference/skills/experimental.md @@ -37,6 +37,34 @@ 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 Management Skill + +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: `"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 + - 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 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/.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..39583c9306 --- /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 ITSMSkill.Responses.Shared; +using ITSMSkill.Services; +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; + +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(DialogState)))); + } + } +} 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..d821ed779e --- /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 ITSMSkill.Responses.Shared; +using ITSMSkill.Services; +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; + +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..fae9cb6cf7 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.1.0.json @@ -0,0 +1,59 @@ +{ + "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": "{Number}" + } + ] + }, + { + "type": "TextBlock", + "wrap": true, + "text": "{Content}", + "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 new file mode 100644 index 0000000000..669e7831a2 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Content/Knowledge.json @@ -0,0 +1,75 @@ +{ + "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": "{Number}" + } + ] + }, + { + "type": "TextBlock", + "wrap": true, + "text": "{Content}", + "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/Ticket.1.0.json b/skills/src/csharp/experimental/itsmskill/Content/Ticket.1.0.json new file mode 100644 index 0000000000..18bf7652f8 --- /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": "{Number}" + } + ] + }, + { + "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..4ae89240f1 --- /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": "{Number}" + } + ] + }, + { + "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..5fb83848fc --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Controllers/BotController.cs @@ -0,0 +1,26 @@ +// 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.Skills.Auth; +using Microsoft.Bot.Builder.Solutions; + +namespace ITSMSkill.Controllers +{ + [ApiController] + public class BotController : SkillController + { + public BotController( + IBot bot, + BotSettingsBase botSettings, + IBotFrameworkHttpAdapter botFrameworkHttpAdapter, + SkillWebSocketAdapter skillWebSocketAdapter, + IWhitelistAuthenticationProvider whitelistAuthenticationProvider) + : base(bot, botSettings, botFrameworkHttpAdapter, skillWebSocketAdapter, whitelistAuthenticationProvider) + { + } + } +} \ No newline at end of file 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/LU/en/ITSM.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu new file mode 100644 index 0000000000..81204ebdd8 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/ITSM.lu @@ -0,0 +1,57 @@ +[Ticket Create](./Ticket/Create.lu) +[Ticket Update](./Ticket/Update.lu) +[Ticket Show](./Ticket/Show.lu) +[Ticket Close](./Ticket/Close.lu) +[Knowledge Show](./Knowledge/Show.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 + +$CloseReason:simple + +> # List entities + +$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= + +$TicketNumber:/INC[0-9]{7}/ 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/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 new file mode 100644 index 0000000000..47c3f35137 --- /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 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/Show.lu b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Show.lu new file mode 100644 index 0000000000..1df648ff07 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Deployment/Resources/LU/en/Ticket/Show.lu @@ -0,0 +1,20 @@ +# 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} +- search tickets +- search issues +- search incidents 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..a77b7ad20d --- /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 an issue +- update ticket's urgency to high +- update ticket's description to {TicketDescription=can't log} +- change a ticket +- change an incident +- 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 an issue +- i would like to update a ticket +- i would like to update an incident +- i would like to update an issue 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..c0c007cebe --- /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": "5a40ff56-5dad-475a-9aea-f7b3fa0cf17e", + "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/CloseTicketDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs new file mode 100644 index 0000000000..673277a31f --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/CloseTicketDialog.cs @@ -0,0 +1,85 @@ +// 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[] + { + BeginSetNumberThenId, + CheckClosed, + CheckReason, + InputReason, + SetReason, + GetAuthToken, + AfterGetAuthToken, + CloseTicket + }; + + AddDialog(new WaterfallDialog(Actions.CloseTicket, closeTicket)); + + 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()); + var management = ServiceManager.CreateManagement(Settings, state.Token); + var result = await management.CloseTicket(id: state.Id, reason: state.CloseReason); + + if (!result.Success) + { + return await SendServiceErrorAndCancel(sc, result); + } + + 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 new file mode 100644 index 0000000000..7e2c628bf5 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/CreateTicketDialog.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.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.Responses.Ticket; +using ITSMSkill.Services; +using Luis; +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, + DisplayExistingLoop, + CheckUrgency, + InputUrgency, + SetUrgency, + GetAuthToken, + AfterGetAuthToken, + CreateTicket + }; + + var displayExisting = new WaterfallStep[] + { + GetAuthToken, + AfterGetAuthToken, + ShowKnowledge, + IfKnowledgeHelp + }; + + AddDialog(new WaterfallDialog(Actions.CreateTicket, createTicket)); + AddDialog(new WaterfallDialog(Actions.DisplayExisting, displayExisting)); + + InitialDialogId = Actions.CreateTicket; + + // intended null + // ShowKnowledgeNoResponse + ShowKnowledgeEndResponse = KnowledgeResponses.KnowledgeEnd; + ShowKnowledgeResponse = TicketResponses.IfExistingSolve; + ShowKnowledgePrompt = Actions.NavigateYesNoPrompt; + KnowledgeHelpLoop = Actions.DisplayExisting; + } + + protected async Task DisplayExistingLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + + if (state.DisplayExisting) + { + state.PageIndex = -1; + return await sc.BeginDialogAsync(Actions.DisplayExisting); + } + else + { + return await sc.NextAsync(); + } + } + + protected async Task CreateTicket(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.CreateTicket(state.TicketDescription, state.UrgencyLevel); + + if (!result.Success) + { + return await SendServiceErrorAndCancel(sc, result); + } + + var card = new Card() + { + Name = GetDivergedCardName(sc.Context, "Ticket"), + Data = ConvertTicket(result.Tickets[0]) + }; + + 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 new file mode 100644 index 0000000000..209b3009c4 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/MainDialog.cs @@ -0,0 +1,306 @@ +// 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, + UpdateTicketDialog updateTicketDialog, + ShowTicketDialog showTicketDialog, + CloseTicketDialog closeTicketDialog, + ShowKnowledgeDialog showKnowledgeDialog, + 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); + AddDialog(updateTicketDialog); + AddDialog(showTicketDialog); + AddDialog(closeTicketDialog); + AddDialog(showKnowledgeDialog); + } + + protected override async Task OnStartAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) + { + 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 != null && intent != ITSMLuis.Intent.None) + { + var state = await _stateAccessor.GetAsync(dc.Context, () => new SkillState()); + state.DigestLuisResult(result, (ITSMLuis.Intent)intent); + } + + switch (intent) + { + case ITSMLuis.Intent.TicketCreate: + { + turnResult = await dc.BeginDialogAsync(nameof(CreateTicketDialog)); + break; + } + + case ITSMLuis.Intent.TicketUpdate: + { + turnResult = await dc.BeginDialogAsync(nameof(UpdateTicketDialog)); + break; + } + + case ITSMLuis.Intent.TicketShow: + { + turnResult = await dc.BeginDialogAsync(nameof(ShowTicketDialog)); + break; + } + + case ITSMLuis.Intent.TicketClose: + { + turnResult = await dc.BeginDialogAsync(nameof(CloseTicketDialog)); + break; + } + + case ITSMLuis.Intent.KnowledgeShow: + { + turnResult = await dc.BeginDialogAsync(nameof(ShowKnowledgeDialog)); + 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 || Channel.GetChannelId(dc.Context) != Channels.Msteams) + { + var response = dc.Context.Activity.CreateReply(); + response.Type = ActivityTypes.Handoff; + 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.Handoff; + + 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 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: + { + result = await OnCancel(dc); + break; + } + + case GeneralLuis.Intent.Help: + { + result = await OnHelp(dc); + break; + } + + case GeneralLuis.Intent.Logout: + { + result = await OnLogout(dc); + break; + } + } + } + else + { + state.GeneralIntent = GeneralLuis.Intent.None; + } + } + } + + 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/ShowKnowledgeDialog.cs b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs new file mode 100644 index 0000000000..e94a36f452 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowKnowledgeDialog.cs @@ -0,0 +1,102 @@ +// 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 Luis; +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, + 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 ShowKnowledgeLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.PageIndex = -1; + + return await sc.BeginDialogAsync(Actions.ShowKnowledgeLoop); + } + + protected async Task IfCreateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var options = new PromptOptions() + { + Prompt = ResponseManager.GetResponse(KnowledgeResponses.IfCreateTicket) + }; + + return await sc.PromptAsync(nameof(ConfirmPrompt), options); + } + + protected async Task AfterIfCreateTicket(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + if ((bool)sc.Result) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.DisplayExisting = false; + + return await sc.ReplaceDialogAsync(nameof(CreateTicketDialog)); + } + else + { + 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 new file mode 100644 index 0000000000..7c211909ca --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/ShowTicketDialog.cs @@ -0,0 +1,253 @@ +// 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.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; +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, + ShowTicketLoop + }; + + var showAttribute = new WaterfallStep[] + { + ShowConstraints, + CheckAttribute, + InputAttribute, + SetAttribute, + UpdateSelectedAttribute, + ShowLoop + }; + + var showTicketLoop = new WaterfallStep[] + { + GetAuthToken, + AfterGetAuthToken, + ShowTicket, + IfContinueShow + }; + + 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 }); + AddDialog(new AttributeWithNoPrompt(Actions.ShowAttributePrompt, attributesForShow)); + AddDialog(new WaterfallDialog(Actions.ShowTicketLoop, showTicketLoop) { TelemetryClient = telemetryClient }); + + InitialDialogId = Actions.ShowTicket; + + // never used + // ConfirmAttributeResponse + InputAttributeResponse = TicketResponses.ShowAttribute; + InputAttributePrompt = Actions.ShowAttributePrompt; + } + + protected async Task ShowAttributeLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + 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()); + + // always prompt for search + state.AttributeType = AttributeType.None; + + var sb = new StringBuilder(); + if (!string.IsNullOrEmpty(state.TicketNumber)) + { + sb.AppendLine($"{SharedStrings.TicketNumber}{state.TicketNumber}"); + } + + if (!string.IsNullOrEmpty(state.TicketDescription)) + { + sb.AppendLine($"{SharedStrings.Description}{state.TicketDescription}"); + } + + if (state.UrgencyLevel != UrgencyLevel.None) + { + 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)); + } + else + { + var token = new StringDictionary() + { + { "Attributes", sb.ToString() } + }; + + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.ShowConstraints, token)); + } + + 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()); + + bool firstDisplay = false; + if (state.PageIndex == -1) + { + firstDisplay = true; + state.PageIndex = 0; + } + + var management = ServiceManager.CreateManagement(Settings, state.Token); + var urgencies = new List(); + if (state.UrgencyLevel != UrgencyLevel.None) + { + urgencies.Add(state.UrgencyLevel); + } + + 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, number: state.TicketNumber, states: states); + + if (!result.Success) + { + return await SendServiceErrorAndCancel(sc, result); + } + + if (result.Tickets == null || result.Tickets.Length == 0) + { + 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 + { + var cards = new List(); + foreach (var ticket in result.Tickets) + { + cards.Add(new Card() + { + Name = GetDivergedCardName(sc.Context, "Ticket"), + Data = ConvertTicket(ticket) + }); + } + + 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 new file mode 100644 index 0000000000..e6e2e1aea7 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/SkillDialogBase.cs @@ -0,0 +1,923 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +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 ITSMSkill.Utilities; +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)); + + var setDescription = new WaterfallStep[] + { + CheckDescription, + InputDescription, + SetDescription + }; + + var setUrgency = new WaterfallStep[] + { + CheckUrgency, + InputUrgency, + SetUrgency + }; + + var setId = new WaterfallStep[] + { + CheckId, + InputId, + SetId + }; + + var setState = new WaterfallStep[] + { + CheckState, + InputState, + 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, + AfterGetAuthToken, + BeginInitialDialog + }; + + 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 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)); + + base.InitialDialogId = Actions.BaseAuth; + } + + 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 new string InitialDialogId { get; set; } + + protected string ConfirmAttributeResponse { get; set; } + + protected string InputAttributeResponse { get; set; } + + 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); + } + + 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 + { + 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) + { + state.Token = providerTokenResponse.TokenResponse; + } + else + { + 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) + { + 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 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()); + 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 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() + { + Prompt = ResponseManager.GetResponse(InputAttributeResponse) + }; + + return await sc.PromptAsync(InputAttributePrompt, options); + } + else + { + return await sc.NextAsync(state.AttributeType); + } + } + + 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(); + } + + protected async Task UpdateSelectedAttribute(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + 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 if (state.AttributeType == AttributeType.Number) + { + state.TicketNumber = null; + return await sc.BeginDialogAsync(Actions.SetNumber); + } + else + { + throw new Exception($"Invalid AttributeType: {state.AttributeType}"); + } + } + + 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 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 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()); + 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.ToLocalizedString() + }, + new Choice() + { + Value = UrgencyLevel.Medium.ToLocalizedString() + }, + new Choice() + { + Value = UrgencyLevel.High.ToLocalizedString() + } + } + }; + + return await sc.PromptAsync(nameof(ChoicePrompt), options); + } + else + { + // use Index to skip localization + return await sc.NextAsync(new FoundChoice() + { + Index = (int)state.UrgencyLevel - 1 + }); + } + } + + protected async Task SetUrgency(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + state.UrgencyLevel = (UrgencyLevel)(((FoundChoice)sc.Result).Index + 1); + return await sc.NextAsync(); + } + + protected async Task CheckState(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + var state = await StateAccessor.GetAsync(sc.Context, () => new SkillState()); + if (state.TicketState == TicketState.None) + { + 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) + { + firstDisplay = true; + state.PageIndex = 0; + } + + var management = ServiceManager.CreateManagement(Settings, state.Token); + var result = await management.SearchKnowledge(state.TicketDescription, state.PageIndex); + + if (!result.Success) + { + return await SendServiceErrorAndCancel(sc, result); + } + + 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) + { + 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.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() + { + Description = ticket.Description, + 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, + Speak = ticket.Description, + Number = $"{SharedStrings.TicketNumber}{ticket.Number}", + }; + return card; + } + + protected KnowledgeCard ConvertKnowledge(Knowledge knowledge) + { + var card = new KnowledgeCard() + { + Id = $"{SharedStrings.ID}{knowledge.Id}", + Title = knowledge.Title, + UpdatedTime = $"{SharedStrings.UpdatedAt}{knowledge.UpdatedTime.ToString()}", + Content = knowledge.Content, + Speak = knowledge.Title, + Number = $"{SharedStrings.TicketNumber}{knowledge.Number}", + UrlTitle = SharedStrings.OpenKnowledge, + UrlLink = knowledge.Url, + }; + return card; + } + + 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 SetDescription = "SetDescription"; + 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"; + + 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"; + public const string UpdateAttributePrompt = "UpdateAttributePrompt"; + + 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 new file mode 100644 index 0000000000..f2692fb155 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Dialogs/UpdateTicketDialog.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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; +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[] + { + BeginSetNumberThenId, + UpdateAttributeLoop, + GetAuthToken, + AfterGetAuthToken, + UpdateTicket + }; + + var updateAttribute = new WaterfallStep[] + { + ShowUpdates, + CheckAttribute, + InputAttribute, + SetAttribute, + UpdateSelectedAttribute, + 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 AttributeWithNoPrompt(Actions.UpdateAttributePrompt, attributesForUpdate)); + + InitialDialogId = Actions.UpdateTicket; + + ConfirmAttributeResponse = TicketResponses.ConfirmUpdateAttribute; + InputAttributeResponse = TicketResponses.UpdateAttribute; + InputAttributePrompt = Actions.UpdateAttributePrompt; + } + + protected async Task UpdateAttributeLoop(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + 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 + { + var token = new StringDictionary() + { + { "Attributes", sb.ToString() } + }; + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(TicketResponses.ShowUpdates, token)); + } + + 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 from Luis should be used first + 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()); + + 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) + { + return await SendServiceErrorAndCancel(sc, result); + } + + 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 new file mode 100644 index 0000000000..763a07da26 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/ITSMSkill.csproj @@ -0,0 +1,152 @@ + + + + netcoreapp2.2 + NU1701 + + https://botbuilder.myget.org/F/aitemplates/api/v3/index.json; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextTemplatingFileGenerator + TicketResponses.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + + TextTemplatingFileGenerator + KnowledgeResponses.cs + + + TextTemplatingFileGenerator + TicketResponses.cs + + + TextTemplatingFileGenerator + MainResponses.cs + + + TextTemplatingFileGenerator + MainResponses.cs + + + TextTemplatingFileGenerator + SharedResponses.cs + + + + + + + + + + + + + + True + True + KnowledgeResponses.tt + + + True + True + TicketResponses.tt + + + True + True + MainResponses.tt + + + True + True + MainResponses.tt + + + True + True + SharedResponses.tt + + + True + True + SharedStrings.resx + + + True + True + TicketResponses.tt + + + + + + PublicResXFileCodeGenerator + SharedStrings.Designer.cs + + + + diff --git a/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs b/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs new file mode 100644 index 0000000000..194fd24a5b --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/AttributeType.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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, + [EnumLocalizedDescription("AttributeState", typeof(SharedStrings))] + State, + [EnumLocalizedDescription("AttributeNumber", typeof(SharedStrings))] + Number, + } +} 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..83ee78aa76 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/Knowledge.cs @@ -0,0 +1,25 @@ +// 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; + +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; } + + 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 new file mode 100644 index 0000000000..1a5a8fec23 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/KnowledgeCard.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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; } + + public string Number { get; set; } + + public string UrlTitle { get; set; } + + public string UrlLink { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/KnowledgesResult.cs b/skills/src/csharp/experimental/itsmskill/Models/KnowledgesResult.cs new file mode 100644 index 0000000000..3f09cb8dda --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/KnowledgesResult.cs @@ -0,0 +1,15 @@ +// 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; + +namespace ITSMSkill.Models +{ + public class KnowledgesResult : ResultBase + { + public Knowledge[] Knowledges { 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..9edced93d7 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ResultBase.cs @@ -0,0 +1,17 @@ +// 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; + +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/ServiceNow/CreateTicketRequest.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs new file mode 100644 index 0000000000..48bc878ccb --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/CreateTicketRequest.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace ITSMSkill.Models.ServiceNow +{ + 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..790b0ec768 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/GetUserIdResponse.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace ITSMSkill.Models.ServiceNow +{ + public class GetUserIdResponse + { + public string 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..92d3875866 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/KnowledgeResponse.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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; } + + public string number { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiKnowledgesResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiKnowledgesResponse.cs new file mode 100644 index 0000000000..9fa1309885 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiKnowledgesResponse.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace ITSMSkill.Models.ServiceNow +{ + public class MultiKnowledgesResponse + { + public List result { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiTicketsResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiTicketsResponse.cs new file mode 100644 index 0000000000..c0bb5a3a6c --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/MultiTicketsResponse.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace ITSMSkill.Models.ServiceNow +{ + public class MultiTicketsResponse + { + public List result { get; set; } + } +} diff --git a/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SingleTicketResponse.cs b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SingleTicketResponse.cs new file mode 100644 index 0000000000..e36b1175ed --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/SingleTicketResponse.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace ITSMSkill.Models.ServiceNow +{ + public class SingleTicketResponse + { + public TicketResponse 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..721a2ae93b --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/ServiceNow/TicketResponse.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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 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 new file mode 100644 index 0000000000..e23a728433 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/SkillState.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using ITSMSkill.Services; +using Luis; +using Microsoft.Bot.Schema; + +namespace ITSMSkill.Models +{ + public class SkillState + { + public SkillState() + { + 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 DisplayExisting { get; set; } + + public Ticket TicketTarget { get; set; } + + public string Id { get; set; } + + public string TicketDescription { get; set; } + + public string CloseReason { get; set; } + + public UrgencyLevel UrgencyLevel { get; set; } + + public AttributeType AttributeType { get; set; } + + public TicketState TicketState { get; set; } + + // INC[0-9]{7} + public string TicketNumber { get; set; } + + // from OnInterruptDialogAsync + public GeneralLuis.Intent GeneralIntent { get; set; } + + public void DigestLuisResult(ITSMLuis luis, ITSMLuis.Intent topIntent) + { + ClearLuisResult(); + + if (luis.Entities.TicketDescription != null) + { + 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) + { + UrgencyLevel = Enum.Parse(luis.Entities.UrgencyLevel[0][0], true); + } + + if (luis.Entities.AttributeType != null) + { + AttributeType = Enum.Parse(luis.Entities.AttributeType[0][0], true); + } + + if (luis.Entities.TicketState != null) + { + 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) + { + // 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; + } + } + else if (topIntent == ITSMLuis.Intent.TicketCreate) + { + DisplayExisting = true; + } + else if (topIntent == ITSMLuis.Intent.TicketShow) + { + AttributeType = AttributeType.None; + } + } + + public void ClearLuisResult() + { + Id = null; + TicketDescription = null; + CloseReason = null; + 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 new file mode 100644 index 0000000000..e1c9efb5f0 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/Ticket.cs @@ -0,0 +1,27 @@ +// 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; + +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; } + + 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 new file mode 100644 index 0000000000..dab52a7d54 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/TicketCard.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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; } + + public string Number { 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..1ca0ed3c45 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/TicketState.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using ITSMSkill.Responses.Shared; +using ITSMSkill.Utilities; + +namespace ITSMSkill.Models +{ + // TODO same as ServiceNow's ticket state + 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/TicketsResult.cs b/skills/src/csharp/experimental/itsmskill/Models/TicketsResult.cs new file mode 100644 index 0000000000..91611d8ad5 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/TicketsResult.cs @@ -0,0 +1,15 @@ +// 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; + +namespace ITSMSkill.Models +{ + public class TicketsResult : ResultBase + { + public Ticket[] Tickets { get; set; } + } +} 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..a1647bd3fc --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Models/UrgencyLevel.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using ITSMSkill.Responses.Shared; +using ITSMSkill.Utilities; + +namespace ITSMSkill.Models +{ + // TODO same as ServiceNow's Urgency. However it is mapped to Priority internally + 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/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/Prompts/AttributeWithNoPrompt.cs b/skills/src/csharp/experimental/itsmskill/Prompts/AttributeWithNoPrompt.cs new file mode 100644 index 0000000000..8d5f754515 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Prompts/AttributeWithNoPrompt.cs @@ -0,0 +1,99 @@ +// 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 ITSMSkill.Responses.Shared; +using ITSMSkill.Utilities; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Solutions.Util; +using Microsoft.Bot.Schema; + +namespace ITSMSkill.Prompts +{ + public class AttributeWithNoPrompt : Prompt + { + private readonly AttributeType[] attributes; + + public AttributeWithNoPrompt(string dialogId, AttributeType[] attributes, PromptValidator validator = null, string defaultLocale = null) + : base(dialogId, validator) + { + 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(); + + var promptRecognizerResult = ConfirmRecognizerHelper.ConfirmYesOrNo(message.Text, turnContext.Activity.Locale); + if (promptRecognizerResult.Succeeded && !promptRecognizerResult.Value) + { + result.Succeeded = true; + 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.None: return false; + default: return message.Equals(attribute.ToLocalizedString()); + } + } + } +} 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/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/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/Knowledge/KnowledgeResponses.cs b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs new file mode 100644 index 0000000000..500b2ba3fe --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.cs @@ -0,0 +1,20 @@ +// 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 IfFindWanted = "IfFindWanted"; + public const string IfCreateTicket = "IfCreateTicket"; + 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 new file mode 100644 index 0000000000..276d553092 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Knowledge/KnowledgeResponses.json @@ -0,0 +1,38 @@ +{ + "IfFindWanted": { + "replies": [ + { + "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" + }, + "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 previous\".", + "speak": "Page {Page} does not contain anything, try \"Go previous\"." + } + ], + "inputHint": "acceptingInput" + }, + "KnowledgeShowNone": { + "replies": [ + { + "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/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/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..7708c37652 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Main/MainResponses.json @@ -0,0 +1,83 @@ +{ + "WelcomeMessage": { + "replies": [ + { + "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": [], + "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..d89353ecb6 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.cs @@ -0,0 +1,34 @@ +// 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 ConfirmReason = "ConfirmReason"; + 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 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 new file mode 100644 index 0000000000..16297db4a9 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedResponses.json @@ -0,0 +1,256 @@ +{ + "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 anything else.", + "speak": "Let me know if you need my help with anything 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 provide the description:", + "speak": "Please provide the description:" + } + ], + "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": [ + { + "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" + }, + "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": [ + { + "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" + }, + "InputTicketNumber": { + "replies": [ + { + "text": "Please provide the ticket number:", + "speak": "Please provide the ticket number:" + } + ], + "inputHint": "expectingInput" + }, + "ServiceFailed": { + "replies": [ + { + "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/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..7c1110184f --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.Designer.cs @@ -0,0 +1,261 @@ +//------------------------------------------------------------------------------ +// +// 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 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 number. + /// + public static string AttributeNumber { + get { + return ResourceManager.GetString("AttributeNumber", resourceCulture); + } + } + + /// + /// 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. + /// + public static string AttributeUrgency { + get { + return ResourceManager.GetString("AttributeUrgency", resourceCulture); + } + } + + /// + /// 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 . + /// + public static string OpenedAt { + get { + return ResourceManager.GetString("OpenedAt", resourceCulture); + } + } + + /// + /// 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: . + /// + public static string TicketNumber { + get { + return ResourceManager.GetString("TicketNumber", 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..5b798c2c21 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Shared/SharedStrings.resx @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + description + + + id + + + urgency + + + Opened at + + + State: + + + Updated at + + + Urgency: + + + Description: + + + ID: + + + state + + + Number: + + + number + + + Open Article + + \ 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 new file mode 100644 index 0000000000..d7d670bdae --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.cs @@ -0,0 +1,34 @@ +// 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.Ticket +{ + /// + /// Contains bot responses. + /// + 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"; + 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"; + 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 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 new file mode 100644 index 0000000000..4e957c03d5 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.json @@ -0,0 +1,168 @@ +{ + "IfExistingSolve": { + "replies": [ + { + "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" + }, + "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" + }, + "ConfirmUpdateAttribute": { + "replies": [ + { + "text": "Do you want to update {Attribute}?", + "speak": "Do you want to update {Attribute}?" + } + ], + "inputHint": "expectingInput" + }, + "UpdateAttribute": { + // same with SharedStrings + "suggestedActions": [ "description", "urgency", "no" ], + "replies": [ + { + "text": "Which attribute do you want to update?", + "speak": "Which attribute do you want to update?" + } + ], + "inputHint": "expectingInput" + }, + "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" + }, + "TicketNoUpdate": { + "replies": [ + { + "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" + }, + "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:\n{Attributes}", + "speak": "You have the following constraints on search:\n{Attributes}" + } + ], + "inputHint": "igoringInput" + }, + "ShowUpdateNone": { + "replies": [ + { + "text": "You don't have any to update.", + "speak": "You don't have any to update." + } + ], + "inputHint": "igoringInput" + }, + "ShowUpdates": { + "replies": [ + { + "text": "You have the following to update:\n{Attributes}", + "speak": "You have the following to update:\n{Attributes}" + } + ], + "inputHint": "igoringInput" + }, + "ShowAttribute": { + // same with SharedStrings + "suggestedActions": [ "number", "description", "urgency", "state", "no" ], + "replies": [ + { + "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" + }, + "TicketShow": { + "replies": [ + { + "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" + }, + "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" + }, + "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": [ + { + "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/Responses/Ticket/TicketResponses.tt b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.tt new file mode 100644 index 0000000000..f204f0981b --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Responses/Ticket/TicketResponses.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/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..2d437ad99e --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/BotSettings.cs @@ -0,0 +1,16 @@ +// 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 string ServiceNowGetUserId { 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..b75b852a02 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/IITServiceManagement.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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(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); + + Task CloseTicket(string id, string reason); + + Task SearchKnowledge(string query, int pageIndex); + } +} 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..c696fe4a0e --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/IServiceManager.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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..55564674d8 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/ITSMLuis.cs @@ -0,0 +1,93 @@ +// +// 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 + { + [JsonProperty("text")] + public string Text; + + [JsonProperty("alteredText")] + public string AlteredText; + + public enum Intent { + None, + TicketCreate, + TicketUpdate, + TicketShow, + TicketClose, + KnowledgeShow + }; + [JsonProperty("intents")] + public Dictionary Intents; + + public class _Entities + { + // Simple entities + public string[] CloseReason; + + public string[] TicketDescription; + + // Lists + public string[][] AttributeType; + + public string[][] TicketState; + + 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; + } + [JsonProperty("$instance")] + public _Instance _instance; + } + [JsonProperty("entities")] + 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..a6063ed17f --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceManager.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Schema; + +namespace ITSMSkill.Services +{ + public class ServiceManager : IServiceManager + { + public IITServiceManagement CreateManagement(BotSettings botSettings, TokenResponse tokenResponse) + { + if (tokenResponse.ConnectionName == "ServiceNow" && !string.IsNullOrEmpty(botSettings.ServiceNowUrl) && !string.IsNullOrEmpty(botSettings.ServiceNowGetUserId)) + { + return new ServiceNow.Management(botSettings.ServiceNowUrl, tokenResponse.Token, botSettings.LimitSize, botSettings.ServiceNowGetUserId); + } + 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..2d155ef8fc --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Services/ServiceNow/Management.cs @@ -0,0 +1,335 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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 Newtonsoft.Json; +using RestSharp; +using RestSharp.Serializers; + +namespace ITSMSkill.Services.ServiceNow +{ + public class Management : IITServiceManagement + { + 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; + private readonly string knowledgeUrl; + + 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, string getUserIdResource) + { + this.client = new RestClient($"{url}/api/"); + 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) + { + try + { + var request = CreateRequest(getUserIdResource); + var userId = await client.GetAsync(request); + + request = CreateRequest(TicketResource); + var body = new CreateTicketRequest() + { + caller_id = userId.result, + short_description = description, + urgency = UrgencyToString[urgency] + }; + request.AddJsonBody(body); + var result = await client.PostAsync(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 SearchTicket(int pageIndex, string description = null, List urgencies = null, string id = null, List states = null, string number = null) + { + try + { + var request = CreateRequest(getUserIdResource); + var userId = await client.GetAsync(request); + + request = CreateRequest(TicketResource); + + var sysparmQuery = new List + { + $"caller_id={userId.result}" + }; + + 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 (!string.IsNullOrEmpty(number)) + { + sysparmQuery.Add($"number={number}"); + } + + request.AddParameter("sysparm_query", string.Join('^', sysparmQuery)); + + request.AddParameter("sysparm_limit", limitSize); + + request.AddParameter("sysparm_offset", limitSize * pageIndex); + + var result = await client.GetAsync(request); + return new TicketsResult() + { + Success = true, + Tickets = result.result?.Select(r => ConvertTicket(r)).ToArray() + }; + } + catch (Exception ex) + { + return new TicketsResult() + { + Success = false, + ErrorMessage = ex.Message + }; + } + } + + 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 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, int pageIndex) + { + 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); + + request.AddParameter("sysparm_offset", limitSize * pageIndex); + + try + { + var result = await client.GetAsync(request); + return new KnowledgesResult() + { + Success = true, + Knowledges = result.result?.Select(r => ConvertKnowledge(r)).ToArray() + }; + } + catch (Exception ex) + { + return new KnowledgesResult() + { + 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), + Number = ticketResponse.number, + }; + + 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), + Number = knowledgeResponse.number, + Url = string.Format(knowledgeUrl, knowledgeResponse.number), + }; + 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; + } + + 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 new file mode 100644 index 0000000000..ee103eda49 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Startup.cs @@ -0,0 +1,144 @@ +// 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; +using ITSMSkill.Dialogs; +using ITSMSkill.Responses.Knowledge; +using ITSMSkill.Responses.Main; +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; +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.Skills.Auth; +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 TicketResponses(), + new KnowledgeResponses(), + new SharedResponses())); + + // Configure service + services.AddSingleton(new ServiceManager()); + + // Register dialogs + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Configure adapters + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Configure bot + services.AddTransient>(); + + services.AddSingleton(new SimpleWhitelistAuthenticationProvider()); + } + + // 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/Utilities/EnumToLocalizedString.cs b/skills/src/csharp/experimental/itsmskill/Utilities/EnumToLocalizedString.cs new file mode 100644 index 0000000000..088e3d78aa --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/Utilities/EnumToLocalizedString.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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); + } + } + } +} 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 { }; + } +} diff --git a/skills/src/csharp/experimental/itsmskill/appsettings.json b/skills/src/csharp/experimental/itsmskill/appsettings.json new file mode 100644 index 0000000000..bd7ffb646b --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/appsettings.json @@ -0,0 +1,26 @@ +{ + "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", + "serviceNowGetUserId": "namespace/get_user_id", + "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..812b929629 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/manifestTemplate.json @@ -0,0 +1,149 @@ +{ + "id": "itsmSkill", + "name": "ITSM Skill", + "description": "The IT Service Management Skill provides ticket and knowledge base related capabilities and supports SerivceNow.", + "iconUrl": "", + "authenticationConnections": [ + { + "id": "ServiceNow", + "serviceProviderId": "Generic Oauth 2", + "scopes": "" + } + ], + "actions": [ + { + "id": "itsmSkill_createTicket", + "definition": { + "description": "Create a new ticket", + "slots": [ + { + "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" ] + } + ], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "ITSM#KnowledgeShow" + ] + } + ] + } + } + } + ] +} \ 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..5535ee2405 --- /dev/null +++ b/skills/src/csharp/experimental/itsmskill/readme.md @@ -0,0 +1,3 @@ +# 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. 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