Skip to content

Commit

Permalink
Customized virtual assistant template for MS Teams (OfficeDev#83)
Browse files Browse the repository at this point in the history
* Added virtual assistant template and update readme

* updating revew comment

* resolving comments
  • Loading branch information
nebhagat authored Jun 7, 2021
1 parent a36c11d commit 6e429af
Show file tree
Hide file tree
Showing 62 changed files with 106,825 additions and 2 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

| | Sample Name | Description | C# | TypeScript
|:--:|:-------------------|:---------------------------------------------------------------------------------------------------------------------------|:--------|:-------------|
|1|Hello World | Microsoft Teams hello world sample app. |[View][app-hello-world#cs] |[View][app-hello-world#ts]
|1|Hello World | Microsoft Teams hello world sample app. |[View][app-hello-world#cs] |[View][app-hello-world#ts]

## [Tabs samples](https://docs.microsoft.com/microsoftteams/platform/tabs/what-are-tabs)
| | Sample Name | Description | C# | TypeScript | JavaScript
Expand Down Expand Up @@ -48,7 +48,7 @@
|:--:|:-------------------|:---------------------------------------------------------------------------------|:--------|:-------------|:-------------|
|1|Proactive Messaging | Sample to highlight solutions to two challenges with building proactive messaging apps in Microsoft Teams. |[View][bot-proactive-msg#cs] |
|2| Sharepoint List Bot| This sample app shows the interaction between teams bot and SharePoint List, Bot saves the specified details in SharePoint List as back-end| [View][bot-sharepoint-list#cs] | [View]() | [View]() |

|3|Teams Virtual Assistant| Customized virtual assistant template to support teams capabilities. |[View][app-virtual-assistant#cs]|

## [Messaging Extensions samples](https://docs.microsoft.com/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions) (using the v4 SDK)
>NOTE:
Expand Down Expand Up @@ -151,6 +151,7 @@
[tab-sso#ts]:samples/tabs-sso/nodejs
[tab-sso#cs]:samples/tab-sso/csharp

[app-virtual-assistant#cs]:samples/app-virtual-assistant/csharp
[bot-proactive-msg#cs]:samples/bot-proactive-messaging/csharp
[bot-conversation-quickstart#js]:samples/bot-conversation-quickstart/js
[bot-conversation-sso-quickstart#js]:samples/bot-conversation-sso-quickstart/js
Expand Down
16 changes: 16 additions & 0 deletions samples/app-virtual-assistant/csharp/.filenesting.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"help": "https://go.microsoft.com/fwlink/?linkid=866610",
"dependentFileProviders": {
"add": {
"pathSegment": {
"add": {
".*": [
".json",
".lg",
".resx"
]
}
}
}
}
}
48 changes: 48 additions & 0 deletions samples/app-virtual-assistant/csharp/Adapters/DefaultAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Azure;
using Microsoft.Bot.Builder.Integration.ApplicationInsights.Core;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Solutions.Feedback;
using Microsoft.Bot.Solutions.Middleware;
using Microsoft.Bot.Solutions.Responses;
using Microsoft.Teams.Apps.VirtualAssistant.Services;

namespace Microsoft.Teams.Apps.VirtualAssistant.Adapters
{
public class DefaultAdapter : BotFrameworkHttpAdapter
{
public DefaultAdapter(
BotSettings settings,
ICredentialProvider credentialProvider,
IChannelProvider channelProvider,
LocaleTemplateEngineManager templateEngine,
ConversationState conversationState,
TelemetryInitializerMiddleware telemetryMiddleware,
IBotTelemetryClient telemetryClient)
: base(credentialProvider, channelProvider)
{
OnTurnError = async (turnContext, exception) =>
{
await turnContext.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"Exception Message: {exception.Message}, Stack: {exception.StackTrace}"));
await turnContext.SendActivityAsync(templateEngine.GenerateActivityForLocale("ErrorMessage"));
telemetryClient.TrackException(exception);
};

Use(telemetryMiddleware);

// 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 ShowTypingMiddleware());
Use(new SetLocaleMiddleware(settings.DefaultLocale ?? "en-us"));
Use(new EventDebuggerMiddleware());
Use(new FeedbackMiddleware(conversationState, telemetryClient, new FeedbackOptions()));
Use(new SetSpeakMiddleware());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Solutions.Skills;

namespace Microsoft.Teams.Apps.VirtualAssistant.Authentication
{
/// <summary>
/// Sample claims validator that loads an allowed list from configuration if present
/// and checks that responses are coming from configured skills.
/// </summary>
public class AllowedCallersClaimsValidator : ClaimsValidator
{
private readonly List<string> _allowedSkills;

public AllowedCallersClaimsValidator(SkillsConfiguration skillsConfig)
{
if (skillsConfig == null)
{
throw new ArgumentNullException(nameof(skillsConfig));
}

// Load the appIds for the configured skills (we will only allow responses from skills we have configured).
_allowedSkills = (from skill in skillsConfig.Skills.Values select skill.AppId).ToList();
}

public override Task ValidateClaimsAsync(IList<Claim> claims)
{
if (SkillValidation.IsSkillClaim(claims))
{
// Check that the appId claim in the skill request is in the list of skills configured for this bot.
var appId = JwtTokenValidation.GetAppIdFromClaims(claims);
if (!_allowedSkills.Contains(appId))
{
throw new UnauthorizedAccessException($"Received a request from an application with an appID of \"{appId}\". To enable requests from this skill, add the skill to your configuration file.");
}
}

return Task.CompletedTask;
}
}
}
222 changes: 222 additions & 0 deletions samples/app-virtual-assistant/csharp/Bots/DefaultActivityHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Integration.AspNet.Core.Skills;
using Microsoft.Bot.Builder.Teams;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Schema.Teams;
using Microsoft.Bot.Solutions;
using Microsoft.Bot.Solutions.Responses;
using Microsoft.Bot.Solutions.Skills;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Teams.Apps.VirtualAssistant.Extension;
using Microsoft.Teams.Apps.VirtualAssistant.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.Teams.Apps.VirtualAssistant.Bots
{
public class DefaultActivityHandler<T> : TeamsActivityHandler
where T : Dialog
{
private readonly Dialog _dialog;
private readonly BotState _conversationState;
private IStatePropertyAccessor<DialogState> _dialogStateAccessor;
private LocaleTemplateEngineManager _templateEngine;
private readonly SkillHttpClient _skillHttpClient;
private readonly SkillsConfiguration _skillsConfig;
private readonly IBotTelemetryClient _telemetryClient;
private readonly string _appId;
private readonly string _composeExtensionCommandIdSeparator;

public DefaultActivityHandler(IServiceProvider serviceProvider, T dialog, string appId)
{
_dialog = dialog;
_conversationState = serviceProvider.GetService<ConversationState>();
_dialogStateAccessor = _conversationState.CreateProperty<DialogState>(nameof(DialogState));
_templateEngine = serviceProvider.GetService<LocaleTemplateEngineManager>();
_skillHttpClient = serviceProvider.GetService<SkillHttpClient>();
_skillsConfig = serviceProvider.GetService<SkillsConfiguration>();
_telemetryClient = serviceProvider.GetService<IBotTelemetryClient>();
this._appId = appId;
_composeExtensionCommandIdSeparator = ":";
}

public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
await base.OnTurnAsync(turnContext, cancellationToken);

// Save any state changes that might have occured during the turn.
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}

protected override Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
return _dialog.RunAsync(turnContext, _dialogStateAccessor, cancellationToken);
}

protected override Task OnTeamsSigninVerifyStateAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
return _dialog.RunAsync(turnContext, _dialogStateAccessor, cancellationToken);
}

protected override async Task OnEventActivityAsync(ITurnContext<IEventActivity> turnContext, CancellationToken cancellationToken)
{
var ev = turnContext.Activity.AsEventActivity();
var value = ev.Value?.ToString();

switch (ev.Name)
{
case TokenEvents.TokenResponseEventName:
{
// Forward the token response activity to the dialog waiting on the stack.
await _dialog.RunAsync(turnContext, _dialogStateAccessor, cancellationToken);
break;
}

default:
{
await turnContext.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"Unknown Event '{ev.Name ?? "undefined"}' was received but not processed."));
break;
}
}
}

protected override async Task OnEndOfConversationActivityAsync(ITurnContext<IEndOfConversationActivity> turnContext, CancellationToken cancellationToken)
{
await _dialog.RunAsync(turnContext, _dialogStateAccessor, cancellationToken);
}

// Invoked when a "task/fetch" event is received to invoke task module.
protected override async Task<TaskModuleResponse> OnTeamsTaskModuleFetchAsync(ITurnContext<IInvokeActivity> turnContext, TaskModuleRequest taskModuleRequest, CancellationToken cancellationToken)
{
try
{
string skillId = (turnContext.Activity as Activity).GetSkillId();
var skill = _skillsConfig.Skills.Where(s => s.Value.AppId == skillId).FirstOrDefault().Value;

// Forward request to correct skill
var invokeResponse = await _skillHttpClient.PostActivityAsync(this._appId, skill, _skillsConfig.SkillHostEndpoint, turnContext.Activity as Activity, cancellationToken);

return invokeResponse.GetTaskModuleRespose();
}
catch (Exception exception)
{
await turnContext.SendActivityAsync(_templateEngine.GenerateActivityForLocale("ErrorMessage"));
_telemetryClient.TrackException(exception);

return null;
}
}

// Invoked when a 'task/submit' invoke activity is received for task module submit actions.
protected override async Task<TaskModuleResponse> OnTeamsTaskModuleSubmitAsync(ITurnContext<IInvokeActivity> turnContext, TaskModuleRequest taskModuleRequest, CancellationToken cancellationToken)
{
try
{
string skillId = (turnContext.Activity as Activity).GetSkillId();
var skill = _skillsConfig.Skills.Where(s => s.Value.AppId == skillId).FirstOrDefault().Value;

// Forward request to correct skill
var invokeResponse = await _skillHttpClient.PostActivityAsync(this._appId, skill, _skillsConfig.SkillHostEndpoint, turnContext.Activity as Activity, cancellationToken).ConfigureAwait(false);

return invokeResponse.GetTaskModuleRespose();
}
catch (Exception exception)
{
await turnContext.SendActivityAsync(_templateEngine.GenerateActivityForLocale("ErrorMessage"));
_telemetryClient.TrackException(exception);

return null;
}
}

// Invoked when a 'composeExtension/query' invoke activity is received for compose extension query command.
protected override async Task<MessagingExtensionResponse> OnTeamsMessagingExtensionQueryAsync(ITurnContext<IInvokeActivity> turnContext, MessagingExtensionQuery query, CancellationToken cancellationToken)
{
var skillId = ExtractSkillIdFromComposeExtensionQueryCommand(turnContext, query);
var skill = _skillsConfig.Skills.Where(s => s.Value.AppId == skillId).FirstOrDefault().Value;
var invokeResponse = await _skillHttpClient.PostActivityAsync(this._appId, skill, _skillsConfig.SkillHostEndpoint, turnContext.Activity as Activity, cancellationToken).ConfigureAwait(false);

return invokeResponse.GetMessagingExtensionResponse();
}

// Invoked when a 'composeExtension/selectItem' invoke activity is received for compose extension query command.
protected override async Task<MessagingExtensionResponse> OnTeamsMessagingExtensionSelectItemAsync(ITurnContext<IInvokeActivity> turnContext, JObject query, CancellationToken cancellationToken)
{
var data = JsonConvert.DeserializeObject<SkillCardActionData>(query.ToString());
var skill = _skillsConfig.Skills.Where(s => s.Value.AppId == data.SkillId).FirstOrDefault().Value;
var invokeResponse = await _skillHttpClient.PostActivityAsync(this._appId, skill, _skillsConfig.SkillHostEndpoint, turnContext.Activity as Activity, cancellationToken).ConfigureAwait(false);

return invokeResponse.GetMessagingExtensionResponse();
}

// Invoked when a 'composeExtension/submitAction' invoke activity is received for compose extension action command.
protected override async Task<MessagingExtensionActionResponse> OnTeamsMessagingExtensionSubmitActionAsync(ITurnContext<IInvokeActivity> turnContext, MessagingExtensionAction action, CancellationToken cancellationToken)
{
return await ForwardMessagingExtensionActionCommandActivityToSkill(turnContext, action, cancellationToken);
}

// Invoked when a 'composeExtension/submitAction' invoke activity is received for compose extension edit preview action command.
protected override async Task<MessagingExtensionActionResponse> OnTeamsMessagingExtensionBotMessagePreviewEditAsync(ITurnContext<IInvokeActivity> turnContext, MessagingExtensionAction action, CancellationToken cancellationToken)
{
return await ForwardMessagingExtensionActionCommandActivityToSkill(turnContext, action, cancellationToken);
}

// Invoked when a 'composeExtension/submitAction' invoke activity is received for compose extension send preview action command.
protected override async Task<MessagingExtensionActionResponse> OnTeamsMessagingExtensionBotMessagePreviewSendAsync(ITurnContext<IInvokeActivity> turnContext, MessagingExtensionAction action, CancellationToken cancellationToken)
{
return await ForwardMessagingExtensionActionCommandActivityToSkill(turnContext, action, cancellationToken);
}

// Invoked when a 'composeExtension/fetchTask' invoke activity is received for compose extension action command.
protected override async Task<MessagingExtensionActionResponse> OnTeamsMessagingExtensionFetchTaskAsync(ITurnContext<IInvokeActivity> turnContext, MessagingExtensionAction action, CancellationToken cancellationToken)
{
return await ForwardMessagingExtensionActionCommandActivityToSkill(turnContext, action, cancellationToken);
}

// Forwards invoke activity to right skill for compose extension action commands.
private async Task<MessagingExtensionActionResponse> ForwardMessagingExtensionActionCommandActivityToSkill(ITurnContext<IInvokeActivity> turnContext, MessagingExtensionAction action, CancellationToken cancellationToken)
{
var skillId = ExtractSkillIdFromComposeExtensionActionCommand(turnContext, action);
var skill = _skillsConfig.Skills.Where(s => s.Value.AppId == skillId).FirstOrDefault().Value;
var invokeResponse = await _skillHttpClient.PostActivityAsync(this._appId, skill, _skillsConfig.SkillHostEndpoint, turnContext.Activity as Activity, cancellationToken).ConfigureAwait(false);

return invokeResponse.GetMessagingExtensionActionResponse();
}

// Extracts skill Id from messaging extension query command and updates activity value
private string ExtractSkillIdFromComposeExtensionQueryCommand(ITurnContext<IInvokeActivity> turnContext, MessagingExtensionQuery query)
{
var commandArray = query.CommandId.Split(_composeExtensionCommandIdSeparator);
var skillId = commandArray.Last();

// Update activity value by removing skill id before forwarding to the skill.
var activityValue = JsonConvert.DeserializeObject<MessagingExtensionQuery>(turnContext.Activity.Value.ToString());
activityValue.CommandId = string.Join(_composeExtensionCommandIdSeparator, commandArray.Take(commandArray.Length - 1));
turnContext.Activity.Value = activityValue;

return skillId;
}

// Extracts skill Id from messaging extension command and updates activity value
private string ExtractSkillIdFromComposeExtensionActionCommand(ITurnContext<IInvokeActivity> turnContext, MessagingExtensionAction action)
{
var commandArray = action.CommandId.Split(_composeExtensionCommandIdSeparator);
var skillId = commandArray.Last();

// Update activity value by removing skill id before forwarding to the skill.
var activityValue = JsonConvert.DeserializeObject<MessagingExtensionAction>(turnContext.Activity.Value.ToString());
activityValue.CommandId = string.Join(_composeExtensionCommandIdSeparator, commandArray.Take(commandArray.Length - 1));
turnContext.Activity.Value = activityValue;

return skillId;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider",
"Version": "8.13.10627.1",
"GettingStartedDocument": {
"Uri": "https://go.microsoft.com/fwlink/?LinkID=798432"
}
}
Loading

0 comments on commit 6e429af

Please sign in to comment.