创建虚拟助手

虚拟助手是一个 Microsoft 开源模板,可用于创建可靠的对话解决方案,同时保持对用户体验、组织品牌和必要数据的完全控制。 虚拟助理核心模板是将生成虚拟助理所需的 Microsoft 技术(包括 Bot Framework SDK语言理解 (LUIS) QnA Maker)汇集在一起的基本构建基块。 它还汇集了基本功能,包括技能注册、链接帐户、基本对话意向,以向用户提供一系列无缝交互和体验。 此外,模板功能还包括可重用对话技能的丰富示例。 单个技能集成在虚拟助理解决方案中,以启用多个方案。 使用 Bot Framework SDK,技能以源代码形式呈现,使你可以根据需要进行自定义和扩展。 有关 Bot Framework 技能的详细信息,请参阅什么是 Bot Framework 技能。 本文档介绍组织虚拟助理实现注意事项、如何创建以 Teams 为中心的虚拟助理、相关示例、代码示例和虚拟助理限制。 下图显示了虚拟助理的概述:

显示虚拟助手概述的示意图。

文本消息活动通过虚拟助理核心使用调度模型路由到关联的技能。

实现注意事项

添加虚拟助理的决定包括许多确定因素,并且对于每个组织都有所不同。 组织虚拟助理实现的支持因素如下所示:

  • 中心团队管理所有员工体验。 它能够构建虚拟助理体验并管理核心体验更新,包括添加新技能。
  • 跨业务职能部门存在多个应用程序,预计未来这一数字还会增长。
  • 现有应用程序可自定义,由组织拥有,并转换为虚拟助理技能。
  • 中心员工体验团队能够影响现有应用的自定义。 它还为将现有应用程序集成为虚拟助理体验中的技能提供了必要的指导。

下图显示了虚拟助理的业务功能:

显示中心团队维护助手和业务职能团队贡献技能的示意图。

创建以 Teams 为中心的虚拟助理

Microsoft 已发布用于构建虚拟助理和技能的 Microsoft 模板 。 使用模板,可以创建虚拟助手,该虚拟助手由基于文本的体验提供支持,支持有限的富卡和操作。 我们增强了模板,包括 Microsoft Teams 平台功能和强大的 Teams 应用体验。 其中一些功能包括对丰富的自适应卡片的支持、TeamsJS v1.x) 中称为任务模块 (对话、团队或群组聊天以及消息扩展。 有关将虚拟助理扩展到 Microsoft Teams 的详细信息,请参阅教程:将虚拟助理扩展到 Microsoft Teams。 下图显示了虚拟助理解决方案的高级关系图:

显示虚拟助手解决方案的示意图。

将自适应卡片添加到虚拟助理

如果要正确调度请求,虚拟助理必须标识正确的 LUIS 模型和与之关联的相应技能。 但是,调度机制不能用于卡操作活动,因为与技能关联的 LUIS 模型针对卡操作文本进行了训练。 卡片操作文本是固定的预定义关键字,不会由用户注释。

通过在卡片操作有效负载中嵌入技能信息来解决此缺点。 每个技能都应嵌入skillIdvalue卡操作的字段中。 必须确保每个卡片操作活动都包含相关技能信息,虚拟助理可以利用此信息进行调度。

必须在构造函数中提供 skillId ,以确保技能信息始终存在于卡片操作中。 以下部分显示了卡片操作数据示例代码:

    public class CardActionData
    {
        public CardActionData(string skillId)
        {
            this.SkillId = skillId;
        }

        [JsonProperty("skillId")]
        public string SkillId { get; set; }
    }

    ...
    var button = new CardAction
    {
        Type = ActionTypes.MessageBack,
        Title = "Card action button",
        Text = "card action button text",
        Value = new CardActionData(<SkillId>),
    };

接下来,引入虚拟助理模板中的 SkillCardActionData 类以从卡操作有效负载中提取 skillId。 以下部分显示了要从卡操作有效负载中提取skillId的代码片段:

    // Skill Card action data should contain skillId parameter
    // This class is used to deserialize it and get skillId 
    public class SkillCardActionData
    {
        /// <summary>
        /// Gets the ID of the skil that should handle this card
        /// </summary>
        [JsonProperty("skillId")]
        public string SkillId { get; set; }
    }

实现由 Activity 类中的扩展方法完成。 以下部分显示了要从卡操作数据中提取skillId的代码片段:

    public static class ActivityExtensions
    {
        // Fetches skillId from CardAction data if present
        public static string GetSkillId(this Activity activity)
        {
            string skillId = string.Empty;

            try
            {
                if (activity.Type.Equals(ActivityTypes.Message) && activity.Value != null)
                {
                    var data = JsonConvert.DeserializeObject<SkillCardActionData>(activity.Value.ToString());
                    skillId = data.SkillId;
                }
                else if (activity.Type.Equals(ActivityTypes.Invoke) && activity.Value != null)
                {
                    var data = JsonConvert.DeserializeObject<SkillCardActionData>(JObject.Parse(activity.Value.ToString()).SelectToken("data").ToString());
                    skillId = data.SkillId;
                }
            }
            catch
            {
                // If not able to retrive skillId, empty skillId should be returned
            }

            return skillId;
        }
    }

处理中断

虚拟助理可以在用户尝试在其他技能当前处于活动状态时调用技能的情况下处理中断。 TeamsSkillDialogTeamsSwitchSkillDialog 基于 Bot Framework 的 SkillDialogSwitchSkillDialog 引入。 它们使用户能够从卡片操作切换技能体验。 如果要处理此请求,虚拟助理会提示用户发送确认消息以切换技能:

切换到新技能时确认提示的屏幕截图。

处理对话框请求

若要向虚拟助手添加对话功能,虚拟助手活动处理程序中包含两个附加方法: OnTeamsTaskModuleFetchAsyncOnTeamsTaskModuleSubmitAsync。 这些方法侦听虚拟助手中的对话相关活动,识别与请求关联的技能,并将请求转发到已识别的技能。

请求转发通过 SkillHttpClientPostActivityAsync 方法完成。 它将响应返回为 InvokeResponse,该响应被分析并转换为 TaskModuleResponse

    public static TaskModuleResponse GetTaskModuleRespose(this InvokeResponse invokeResponse)
    {
        if (invokeResponse.Body != null)
        {
            return new TaskModuleResponse()
            {
                Task = GetTask(invokeResponse.Body),
            };
        }

        return null;
    }

    private static TaskModuleResponseBase GetTask(object invokeResponseBody)
        {
            JObject resposeBody = (JObject)JToken.FromObject(invokeResponseBody);
            var task = resposeBody.GetValue("task");
            var taskType = task.SelectToken("type").ToString();

            return taskType switch
            {
                "continue" => new TaskModuleContinueResponse()
                {
                    Type = taskType,
                    Value = task.SelectToken("value").ToObject<TaskModuleTaskInfo>(),
                },
                "message" => new TaskModuleMessageResponse()
                {
                    Type = taskType,
                    Value = task.SelectToken("value").ToString(),
                },
                _ => null,
            };
        }

对于卡操作调度和对话响应,也遵循类似的方法。 对话框提取和提交操作数据已更新为包含 skillId。 活动扩展方法 GetSkillId 从有效负载中提取 skillId ,该有效负载提供有关需要调用的技能的详细信息。

以下部分提供了适用于 OnTeamsTaskModuleFetchAsyncOnTeamsTaskModuleSubmitAsync 方法的代码片段:

    // Invoked when a "task/fetch" event is received to invoke dialog.
    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).First().Value;

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

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

            return null;
        }
    }

    // Invoked when a 'task/submit' invoke activity is received for dialog 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).First().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;
        }
    }

此外,必须在虚拟助理的应用清单文件的 部分中包括所有技能域validDomains,以便通过技能调用的对话正确呈现。

处理协作应用作用域

Teams 应用可以存在于多个作用域内,包括一对一聊天、群组聊天和频道。 核心虚拟助理模板专为一对一聊天而设计。 作为载入体验的一部分,虚拟助理提示用户输入名称并保持用户状态。 由于载入体验不适合群组聊天或频道范围,因此它已被删除。

技能应处理多个作用域活动,例如一对一聊天、群聊和频道对话。 如果这些范围中的任何一个不受支持,则技能必须响应相应的消息。

以下处理函数已添加到虚拟助手核心:

  • 可以调用虚拟助理,无需来自群聊或频道的任何短信。
  • 将消息发送到调度模块之前,会清除表达式。 例如,删除机器人所需的 @mention 。
    if (innerDc.Context.Activity.Conversation?.IsGroup == true)
    {
        // Remove bot atmentions for teams/groupchat scope
        innerDc.Context.Activity.RemoveRecipientMention();

        // If bot is invoked without any text, reply with FirstPromptMessage
        if (string.IsNullOrWhiteSpace(innerDc.Context.Activity.Text))
        {
            await innerDc.Context.SendActivityAsync(_templateEngine.GenerateActivityForLocale("FirstPromptMessage"));
            return EndOfTurn;
        }
    }

处理消息扩展插件

消息扩展的命令在应用清单文件中声明。 消息扩展用户界面由这些命令提供支持。 如果要使虚拟助手将附加技能用于为附加技能提供附加信息扩展命令,虚拟助手自己的清单必须包含这些命令。 必须将单个技能清单中的命令添加到虚拟助手清单中。 命令 ID 通过分隔符 :追加技能的应用 ID 来提供有关关联技能的信息。

技能清单文件中的代码片段如下部分所示:

 "composeExtensions": [
    {
        "botId": "<Skil_App_Id>",
        "commands": [
            {
                "id": "searchQuery",
                "context": [ "compose", "commandBox" ],
                "description": "Test command to run query",
                 ....}
         ]
     }
 ]
                 

以下部分显示了相应的虚拟助理清单文件代码片段:

 "composeExtensions": [
    {
        "botId": "<VA_App_Id>",
        "commands": [
            {
                "id": "searchQuery:<skill_id>",
                "context": [ "compose", "commandBox" ],
                "description": "Test command to run query",
                 ....}
         ]
     }
 ]
 

用户调用命令后,虚拟助手可以通过分析命令 ID 来标识关联的技能,通过删除命令 ID 中的额外后缀 :<skill_id> 来更新活动,并将其转发到相应的技能。 技能的代码不需要处理额外的后缀。 因此,可以避免跨技能命令 ID 之间的冲突。 使用此方法,所有上下文中技能的所有搜索和操作命令(如 composecommandBoxmessage)都由虚拟助理提供支持。

    const string MessagingExtensionCommandIdSeparator = ":";

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

    // Forwards invoke activity to right skill for messaging extension action commands.
    private async Task<MessagingExtensionActionResponse> ForwardMessagingExtensionActionCommandActivityToSkill(ITurnContext<IInvokeActivity> turnContext, MessagingExtensionAction action, CancellationToken cancellationToken)
    {
        var skillId = ExtractSkillIdFromMessagingExtensionActionCommand(turnContext, action);
        var skill = _skillsConfig.Skills.Where(s => s.Value.AppId == skillId).First().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 command and updates activity value
    private string ExtractSkillIdFromMessagingExtensionActionCommand(ITurnContext<IInvokeActivity> turnContext, MessagingExtensionAction action)
    {
        var commandArray = action.CommandId.Split(MessagingExtensionCommandIdSeparator);
        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(MessagingExtensionCommandIdSeparator, commandArray, 0 commandArray.Length - 1);
        turnContext.Activity.Value = activityValue;

        return skillId;
    }

某些消息扩展活动不包括命令 ID。 例如,composeExtensions/selectItem 仅包含调用点击操作的值。 为了标识关联的技能, skillId 附加到每个项卡同时形成响应OnTeamsMessagingExtensionQueryAsync。 这类似于向 虚拟助手添加自适应卡片的方法。

    // 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).First().Value;
        var invokeResponse = await _skillHttpClient.PostActivityAsync(this._appId, skill, _skillsConfig.SkillHostEndpoint, turnContext.Activity as Activity, cancellationToken).ConfigureAwait(false);

        return invokeResponse.GetMessagingExtensionResponse();
    }

示例

以下示例演示如何将“书房”应用模板转换为虚拟助手技能:“书房”是一个 Teams,允许用户从当前时间开始快速查找和预订会议室 30、60 或 90 分钟。 默认时间为 30 分钟。 预订会议室机器人的作用域为个人对话或一对一对话。 下图显示预订会议室技能的虚拟助理:

屏幕截图显示具有预订会议室技能的虚拟助理。

以下是为将其转换为附加到虚拟助手的技能而引入的增量更改。 遵循类似的准则将任何现有的 v4 机器人转换为技能。

技能清单

技能清单是公开技能的消息传送终结点、ID、名称和其他相关元数据的 JSON 文件。 此清单不同于用于在 Teams 中上传自定义应用的清单。 虚拟助理需要此文件的路径作为输入来附加技能。 我们已将以下清单添加到机器人的 wwwroot 文件夹。

botskills connect --remoteManifest "<url to skill's manifest>" ..
{
  "$schema": "https://schemas.botframework.com/schemas/skills/skill-manifest-2.1.preview-0.json",
  "$id": "microsoft_teams_apps_bookaroom",
  "name": "microsoft-teams-apps-bookaroom",
  "description": "microsoft-teams-apps-bookaroom description",
  "publisherName": "Your Company",
  "version": "1.1",
  "iconUrl": "<icon url>",
  "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
  "license": "",
  "privacyUrl": "<privacy url>",
  "endpoints": [
    {
      "name": "production",
      "protocol": "BotFrameworkV3",
      "description": "Production endpoint for the skill",
      "endpointUrl": "<endpoint url>",
      "msAppId": "skill app id"
    }
  ],
  "dispatchModels": {
    "languages": {
      "en-us": [
        {
          "id": "microsoft-teams-apps-bookaroom-en",
          "name": "microsoft-teams-apps-bookaroom LU (English)",
          "contentType": "application/lu",
          "url": "file://book-a-meeting.lu",
          "description": "English language model for the skill"
        }
      ]
    }
  },
  "activities": {
    "message": {
      "type": "message",
      "description": "Receives the users utterance and attempts to resolve it using the skill's LU models"
    }
  }
}

LUIS 集成

虚拟助理的调度模型基于附加技能的 LUIS 模型构建。 调度模型标识每个文本活动的意向,并找出与之关联的技能。

虚拟助理在附加技能时,需要将技能的 LUIS 模型 .lu 格式作为输入。 LUIS json 使用 botframework-cli 工具转换为 .lu 格式。

botskills connect --remoteManifest "<url to skill's manifest>" --luisFolder "<path to the folder containing your Skill's .lu files>" --languages "en-us" --cs
npm i -g @microsoft/botframework-cli
bf luis:convert --in <pathToLUIS.json> --out <pathToLuFile>

预订会议室机器人为用户提供了两个主要命令:

  • Book room
  • Manage Favorites

我们通过了解这两个命令构建了 LUIS 模型。 必须在其中填充 cognitivemodels.json相应的密钥。 相应的 LUIS JSON 文件位于此处。 相应的 .lu 文件显示在以下部分中:

> ! Automatically generated by [LUDown CLI](https://github.com/Microsoft/botbuilder-tools/tree/master/Ludown), Tue Mar 31 2020 17:30:32 GMT+0530 (India Standard Time)

> ! Source LUIS JSON file: book-a-meeting.json

> ! Source QnA TSV file: Not Specified

> ! Source QnA Alterations file: Not Specified


> # Intent definitions

## BOOK ROOM
- book a room
- book room
- please book a room
- reserve a room
- i want to book a room
- i want to book a room please
- get me a room please
- get me a room


## MANAGE FAVORITES
- manage favorites
- manage favorite
- please manage my favorite rooms
- manage my favorite rooms please
- manage my favorite rooms
- i want to manage my favorite rooms

## None


> # Entity definitions


> # PREBUILT Entity definitions


> # Phrase list definitions


> # List entities

> # RegEx entities

使用此方法,用户发出的任何命令虚拟助理与 book roommanage favorites 相关的命令都标识为与 Book-a-room 机器人关联的命令,并转发到此技能。 另一方面, Book-a-room room 机器人需要使用 LUIS 模型来了解这些命令(如果这些命令未键入完整)。 例如:I want to manage my favorite rooms

多语言支持

例如,创建一个仅具有英语区域性的 LUIS 模型。 可以创建与其他语言对应的 LUIS 模型,并将条目添加到 cognitivemodels.json

{
  "defaultLocale": "en-us",
  "languageModels": {
    "en-us": {
      "luisAppId": "",
      "luisApiKey": "",
      "luisApiHost": ""
    },
    "<your_language_culture>": {
      "luisAppId": "",
      "luisApiKey": "",
      "luisApiHost": ""
    }
  }
}

并行在 luisFolder 路径中添加相应的 .lu 文件。 文件夹结构应如下所示:

| - luisFolder

        | - en-us

                | - book-a-meeting.lu

        | - your_language_culture

                | - book-a-meeting.lu

若要修改 languages 参数,请更新机器人技能命令,如下所示:

botskills connect --remoteManifest "<url to skill's manifest>" --luisFolder "<path to luisFolder>" --languages "en-us, your_language_culture" --cs

虚拟助理使用 SetLocaleMiddleware 标识当前区域设置并调用相应的调度模型。 机器人框架活动具有此中间件使用的区域设置字段。 也可以对技能使用相同的功能。 书房机器人不使用此中间件,而是从机器人框架活动的 clientInfo 实体获取区域设置。

声明验证

我们添加了 claimsValidator ,以限制调用方使用技能。 如果要允许虚拟助理调用此技能,请使用该特定虚拟助理的应用 ID 从 appsettings 填充AllowedCallers 数组。

"AllowedCallers": [ "<caller_VA1_appId>", "<caller_VA2_appId>" ],

允许的调用方数组可以限制哪些技能使用者可以访问技能。 将单个条目 * 添加到此数组,以接受来自任何技能使用者的调用。

"AllowedCallers": [ "*" ],

有关向技能添加声明验证的详细信息,请参阅将声明验证添加到技能

卡刷新限制

尚不支持通过虚拟助手 (github 问题) 更新活动,例如卡刷新。 因此,我们已将所有卡刷新调用UpdateActivityAsync替换为发布新的卡调用 SendActivityAsync

卡片操作和对话流

若要将卡操作或对话活动转发到关联的技能,技能必须嵌入skillId其中。 Book-a-room机器人卡操作、对话提取和提交操作有效负载已修改为包含skillId为参数。

有关详细信息,请参阅 将自适应卡片添加到虚拟助手

处理群组聊天或频道作用域的活动

Book-a-room bot 专为私人聊天而设计,例如个人聊天或仅一对一作用域。 由于我们自定义了虚拟助理以支持群组聊天和频道作用域,因此必须从频道作用域调用虚拟助理,因此, Book-a-room 机器人必须获取同一作用域的活动。 因此, Book-a-room 机器人进行自定义以处理这些活动。 可以找到 Book-a-room 机器人活动处理程序 OnMessageActivityAsync 方法的签入方式。

    protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
    {
        // Check if activities are from groupchat/ teams scope. This might happen when the bot is consumed by Virtual Assistant.
        if (turnContext.Activity.Conversation.IsGroup == true)
        {
            await ShowNotSupportedInGroupChatCardAsync(turnContext).ConfigureAwait(false);
        }
        else
        {
            ...
        }
    }

还可以使用 Bot Framework Solutions 存储库 中的现有技能,或从头开始创建一个新技能。 有关创建新技能的详细信息,请参阅创建新技能教程。 有关虚拟助理和技能体系结构文档,请参阅 虚拟助手和技能体系结构

虚拟助理限制

  • EndOfConversation: 技能在完成对话时必须发送 endOfConversation 活动。 根据活动,虚拟助理以该特定技能结束上下文,并返回到虚拟助理的根上下文中。 对于“书房机器人”,没有明确的会话结束状态。 因此,我们尚未从机器人发送endOfConversation,当用户想要返回到根上下文时,可以通过命令执行此操作start overBook-a-room
  • 卡片刷新:尚不支持通过虚拟助手刷新卡片。
  • 消息扩展
    • 目前,虚拟助手最多可以支持 10 个消息扩展命令。
    • 消息扩展的配置范围不是单个命令,而是整个扩展本身。 这会通过虚拟助理限制每个技能的配置。
    • 消息扩展命令 ID 的最大长度为 64 字符 ,37 个字符用于嵌入技能信息。 因此,命令 ID 的更新约束限制为 27 个字符。

还可以使用 Bot Framework Solutions 存储库 中的现有技能,或从头开始创建一个新技能。 稍后的教程可在此处找到。 请参阅有关虚拟助手和技能体系结构 的文档

代码示例

示例名称 说明 .NET
更新了 Visual Studio 模板 自定义模板以支持 teams 功能。 View
预订会议室机器人技能代码 让你随时随地快速查找和预订会议室。 View

另请参阅