在技能中使用对话

适用于:SDK v4

本文演示如何创建支持多个操作的技能。 它使用对话支持这些操作。 主对话接收来自技能使用者的初始输入,然后启动相应的操作。 有关为关联的示例代码实现技能使用者的信息,请参阅如何通过对话使用技能

本文假定你已熟悉如何创建技能。 有关如何创建一般技能机器人的信息,请参阅如何实现技能

注意

Bot Framework JavaScript、C# 和 Python SDK 将继续受支持,但 Java SDK 即将停用,最终长期支持将于 2023 年 11 月结束。

使用 Java SDK 构建的现有机器人将继续正常运行。

要生成新的机器人,请考虑使用 Microsoft Copilot Studio 并阅读选择正确的助理解决方案

有关详细信息,请参阅机器人构建的未来

先决条件

注意

语言理解 (LUIS) 将于 2025 年 10 月 1 日停用。 从 2023 年 4 月 1 日开始,将无法创建新的 LUIS 资源。 语言理解的较新版本现已作为 Azure AI 语言的一部分提供。

对话语言理解(CLU)是 Azure AI 语言的一项功能,是 LUIS 的更新版本。 有关 Bot Framework SDK 中的语言理解支持的详细信息,请参阅自然语言理解

关于此示例

skills skillDialog 示例包含下面的两个机器人的项目:

  • 对话根机器人,通过技能对话类使用技能。
  • 对话技能机器人,使用对话处理技能使用者中的活动。 此技能是核心机器人示例的改编版本。 (有关核心机器人的详细信息,请参阅如何向机器人添加自然语言理解。)

本文重点介绍如何在技能机器人中使用对话来管理多个操作。

有关技能使用者机器人的信息,请参阅如何通过对话使用技能

资源

对于部署的机器人,机器人到机器人身份验证要求每个参与的机器人都有有效的标识。 但是,可以使用 Bot Framework Emulator 在本地测试技能和技能使用者,而无需提供标识信息。

若要使技能可供面向用户的机器人使用,请在 Azure 中注册该技能。 有关详细信息,请参阅如何使用 Azure AI 机器人服务注册机器人

或者,技能机器人可以使用航班预订 LUIS 模型。 若要使用此模型,请使用 CognitiveModels/FlightBooking.json 文件来创建、训练和发布 LUIS 模型。

应用程序配置

  1. (可选)将技能的标识信息添加到技能的配置文件。 (如果技能或技能使用者指定标识,则两者都必须指定。)

  2. 如果使用的是 LUIS 模型,请添加 LUIS 应用 ID、API 密钥和 API 主机名。

DialogSkillBot\appsettings.json

{
  "MicrosoftAppType": "",
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",
  "MicrosoftAppTenantId": "",
  "ConnectionName": "",

  "LuisAppId": "",
  "LuisAPIKey": "",
  "LuisAPIHostName": "",

  // This is a comma separate list with the App IDs that will have access to the skill.
  // This setting is used in AllowedCallersClaimsValidator.
  // Examples: 
  //    [ "*" ] allows all callers.
  //    [ "AppId1", "AppId2" ] only allows access to parent bots with "AppId1" and "AppId2".
  "AllowedCallers": [ "*" ]
}

活动路由逻辑

此机器人支持两种不同的功能。 它可以预订航班或获取城市的天气状况。 此外,如果它收到这些上下文之外的消息,则可以使用 LUIS 来尝试解释消息。 技能清单介绍了这些操作及其输入和输出参数以及技能的终结点。 注意,技能可以处理“BookFlight”或“GetWeather”事件。 它还可以处理消息活动。

技能定义活动路由对话,该对话用于根据技能使用者中的初始传入活动选择要启动的操作。 如果已提供,LUIS 模型可以识别初始消息中的预订航班和获取天气信息意向。

预订航班操作是一个多步骤过程,作为单独的对话实现。 操作开始后,传入活动由该对话处理。 获取天气信息操作具有要在完全实现的机器人中替换的占位符逻辑。

活动路由对话包括用来执行以下操作的代码:

技能中使用的对话继承自组件对话类。 有关组件对话的详细信息,请参阅如何管理对话复杂性

初始化对话

活动路由对话包括用于预订航班的子对话。 主瀑布对话包含一个步骤,该步骤将根据收到的初始活动启动操作。

它还接受 LUIS 识别器。 如果已初始化此识别器,则对话将使用它来解释初始消息活动的意向。

DialogSkillBot\Dialogs\ActivityRouterDialog.cs

private readonly DialogSkillBotRecognizer _luisRecognizer;

public ActivityRouterDialog(DialogSkillBotRecognizer luisRecognizer)
    : base(nameof(ActivityRouterDialog))
{
    _luisRecognizer = luisRecognizer;

    AddDialog(new BookingDialog());
    AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[] { ProcessActivityAsync }));

    // The initial child Dialog to run.
    InitialDialogId = nameof(WaterfallDialog);
}

处理初始活动

在主瀑布对话的第一个(且仅有一个)步骤中,技能检查传入活动类型。

  • 事件活动将转发到事件活动处理程序,该处理程序根据事件的名称启动相应的操作。
  • 消息活动将转发到消息活动处理程序,该处理程序在确定要执行的操作之前执行其他处理。

如果技能无法识别传入活动的类型或事件的名称,则将发送错误消息并结束。

DialogSkillBot\Dialogs\ActivityRouterDialog.cs

private async Task<DialogTurnResult> ProcessActivityAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // A skill can send trace activities, if needed.
    await stepContext.Context.TraceActivityAsync($"{GetType().Name}.ProcessActivityAsync()", label: $"Got ActivityType: {stepContext.Context.Activity.Type}", cancellationToken: cancellationToken);

    switch (stepContext.Context.Activity.Type)
    {
        case ActivityTypes.Event:
            return await OnEventActivityAsync(stepContext, cancellationToken);

        case ActivityTypes.Message:
            return await OnMessageActivityAsync(stepContext, cancellationToken);

        default:
            // We didn't get an activity type we can handle.
            await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Unrecognized ActivityType: \"{stepContext.Context.Activity.Type}\".", inputHint: InputHints.IgnoringInput), cancellationToken);
            return new DialogTurnResult(DialogTurnStatus.Complete);
    }
}
// This method performs different tasks based on the event name.
private async Task<DialogTurnResult> OnEventActivityAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var activity = stepContext.Context.Activity;
    await stepContext.Context.TraceActivityAsync($"{GetType().Name}.OnEventActivityAsync()", label: $"Name: {activity.Name}. Value: {GetObjectAsJsonString(activity.Value)}", cancellationToken: cancellationToken);

    // Resolve what to execute based on the event name.
    switch (activity.Name)
    {
        case "BookFlight":
            return await BeginBookFlight(stepContext, cancellationToken);

        case "GetWeather":
            return await BeginGetWeather(stepContext, cancellationToken);

        default:
            // We didn't get an event name we can handle.
            await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Unrecognized EventName: \"{activity.Name}\".", inputHint: InputHints.IgnoringInput), cancellationToken);
            return new DialogTurnResult(DialogTurnStatus.Complete);
    }
}

处理消息活动

如果已配置 LUIS 识别器,则技能将调用 LUIS,然后基于意向启动操作。 如果未配置 LUIS 识别器或意向不受支持,则技能将发送错误消息并结束。

DialogSkillBot\Dialogs\ActivityRouterDialog.cs

// This method just gets a message activity and runs it through LUIS. 
private async Task<DialogTurnResult> OnMessageActivityAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var activity = stepContext.Context.Activity;
    await stepContext.Context.TraceActivityAsync($"{GetType().Name}.OnMessageActivityAsync()", label: $"Text: \"{activity.Text}\". Value: {GetObjectAsJsonString(activity.Value)}", cancellationToken: cancellationToken);

    if (!_luisRecognizer.IsConfigured)
    {
        await stepContext.Context.SendActivityAsync(MessageFactory.Text("NOTE: LUIS is not configured. To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and 'LuisAPIHostName' to the appsettings.json file.", inputHint: InputHints.IgnoringInput), cancellationToken);
    }
    else
    {
        // Call LUIS with the utterance.
        var luisResult = await _luisRecognizer.RecognizeAsync<FlightBooking>(stepContext.Context, cancellationToken);

        // Create a message showing the LUIS results.
        var sb = new StringBuilder();
        sb.AppendLine($"LUIS results for \"{activity.Text}\":");
        var (intent, intentScore) = luisResult.Intents.FirstOrDefault(x => x.Value.Equals(luisResult.Intents.Values.Max()));
        sb.AppendLine($"Intent: \"{intent}\" Score: {intentScore.Score}");

        await stepContext.Context.SendActivityAsync(MessageFactory.Text(sb.ToString(), inputHint: InputHints.IgnoringInput), cancellationToken);

        // Start a dialog if we recognize the intent.
        switch (luisResult.TopIntent().intent)
        {
            case FlightBooking.Intent.BookFlight:
                return await BeginBookFlight(stepContext, cancellationToken);

            case FlightBooking.Intent.GetWeather:
                return await BeginGetWeather(stepContext, cancellationToken);

            default:
                // Catch all for unhandled intents.
                var didntUnderstandMessageText = $"Sorry, I didn't get that. Please try asking in a different way (intent was {luisResult.TopIntent().intent})";
                var didntUnderstandMessage = MessageFactory.Text(didntUnderstandMessageText, didntUnderstandMessageText, InputHints.IgnoringInput);
                await stepContext.Context.SendActivityAsync(didntUnderstandMessage, cancellationToken);
                break;
        }
    }

    return new DialogTurnResult(DialogTurnStatus.Complete);
}

开始多步骤操作

预订航班操作启动多步骤对话,以获取用户的预订详细信息。

未实现获取天气信息操作。 目前,它将发送占位符消息并结束。

DialogSkillBot\Dialogs\ActivityRouterDialog.cs

private async Task<DialogTurnResult> BeginBookFlight(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var activity = stepContext.Context.Activity;
    var bookingDetails = new BookingDetails();
    if (activity.Value != null)
    {
        bookingDetails = JsonConvert.DeserializeObject<BookingDetails>(JsonConvert.SerializeObject(activity.Value));
    }

    // Start the booking dialog.
    var bookingDialog = FindDialog(nameof(BookingDialog));
    return await stepContext.BeginDialogAsync(bookingDialog.Id, bookingDetails, cancellationToken);
}
private static async Task<DialogTurnResult> BeginGetWeather(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var activity = stepContext.Context.Activity;
    var location = new Location();
    if (activity.Value != null)
    {
        location = JsonConvert.DeserializeObject<Location>(JsonConvert.SerializeObject(activity.Value));
    }

    // We haven't implemented the GetWeatherDialog so we just display a TODO message.
    var getWeatherMessageText = $"TODO: get weather for here (lat: {location.Latitude}, long: {location.Longitude}";
    var getWeatherMessage = MessageFactory.Text(getWeatherMessageText, getWeatherMessageText, InputHints.IgnoringInput);
    await stepContext.Context.SendActivityAsync(getWeatherMessage, cancellationToken);
    return new DialogTurnResult(DialogTurnStatus.Complete);
}

返回结果

技能为预订航班操作启动预订对话。 由于活动路由对话只包含一个步骤,因此当预订对话结束时,活动路由对话也会结束,而预订对话的对话结果将成为活动路由对话的对话结果。

无需设置返回值,获取天气信息操作即可结束。

取消多步骤操作

预订对话及其子日期解析程序对话均派生自基本的“取消和帮助”对话,该对话将检查来自用户的消息。

  • 单击“帮助”或“?”时,它将显示一条帮助消息,然后在下一轮继续对话流。
  • 单击“取消”或“退出”时,它将取消所有对话,这会结束技能。

有关详细信息,请参阅如何处理用户中断

服务注册

此技能所需的服务与一般技能使用者所需的服务相同。 有关所需服务的讨论,请参阅如何实现技能

技能清单

技能清单是一个 JSON 文件,用于描述技能可以执行的活动、其输入和输出参数以及技能的终结点。 清单包含从其他机器人访问技能所需的信息。

DialogSkillBot\wwwroot\manifest\dialogchildbot-manifest-1.0.json

{
  "$schema": "https://schemas.botframework.azure.cn/schemas/skills/skill-manifest-2.0.0.json",
  "$id": "DialogSkillBot",
  "name": "Skill bot with dialogs",
  "version": "1.0",
  "description": "This is a sample skill definition for multiple activity types.",
  "publisherName": "Microsoft",
  "privacyUrl": "https://dialogskillbot.contoso.com/privacy.html",
  "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
  "license": "",
  "iconUrl": "https://dialogskillbot.contoso.com/icon.png",
  "tags": [
    "sample",
    "travel",
    "weather",
    "luis"
  ],
  "endpoints": [
    {
      "name": "default",
      "protocol": "BotFrameworkV3",
      "description": "Default endpoint for the skill.",
      "endpointUrl": "https://dialogskillbot.contoso.com/api/messages",
      "msAppId": "00000000-0000-0000-0000-000000000000"
    }
  ],
  "activities": {
    "bookFlight": {
      "description": "Books a flight (multi turn).",
      "type": "event",
      "name": "BookFlight",
      "value": {
        "$ref": "#/definitions/bookingInfo"
      },
      "resultValue": {
        "$ref": "#/definitions/bookingInfo"
      }
    },
    "getWeather": {
      "description": "Retrieves and returns the weather for the user's location.",
      "type": "event",
      "name": "GetWeather",
      "value": {
        "$ref": "#/definitions/location"
      },
      "resultValue": {
        "$ref": "#/definitions/weatherReport"
      }
    },
    "passthroughMessage": {
      "type": "message",
      "description": "Receives the user's utterance and attempts to resolve it using the skill's LUIS models.",
      "value": {
        "type": "object"
      }
    }
  },
  "definitions": {
    "bookingInfo": {
      "type": "object",
      "required": [
        "origin"
      ],
      "properties": {
        "origin": {
          "type": "string",
          "description": "This is the origin city for the flight."
        },
        "destination": {
          "type": "string",
          "description": "This is the destination city for the flight."
        },
        "travelDate": {
          "type": "string",
          "description": "The date for the flight in YYYY-MM-DD format."
        }
      }
    },
    "weatherReport": {
      "type": "array",
      "description": "Array of forecasts for the next week.",
      "items": [
        {
          "type": "string"
        }
      ]
    },
    "location": {
      "type": "object",
      "description": "Location metadata.",
      "properties": {
        "latitude": {
          "type": "number",
          "title": "Latitude"
        },
        "longitude": {
          "type": "number",
          "title": "Longitude"
        },
        "postalCode": {
          "type": "string",
          "title": "Postal code"
        }
      }
    }
  }
}

技能清单架构是一个 JSON 文件,用于描述技能清单的架构。 最新架构版本为 v2.1

测试技能机器人

可以使用技能使用者测试模拟器中的技能。 为此,需要同时运行技能和技能使用者机器人。 有关如何配置技能的信息,请参阅如何通过对话使用技能

下载并安装最新的 Bot Framework Emulator

  1. 在计算机上以本地方式运行对话技能机器人和对话根机器人。 如需说明,请参阅 C#JavaScriptJavaPython 的示例的 README 文件。
  2. 使用模拟器测试机器人。
    • 第一次加入对话时,机器人会显示欢迎消息,并询问你要调用的技能。 此示例的技能机器人只包含一项技能。
    • 选择“DialogSkillBot”。
  3. 接下来,机器人会要求你为技能选择一项操作。 选择“BookFlight”。
    1. 技能开始预订航班操作;回答提示。
    2. 技能完成后,根机器人会显示预订详细信息,然后会再次提示你要调用的技能。
  4. 再次选择“DialogSkillBot”和“BookFlight”。
    1. 回答第一个提示,然后输入“cancel”以取消操作。
    2. 技能机器人结束时未完成此操作,使用者会提示你要调用的技能。

有关调试的更多信息

由于技能与技能使用者之间的流量已经过身份验证,因此调试此类机器人时会执行额外的步骤。

  • 技能使用者及其直接或间接使用的所有技能都必须处于运行中。
  • 如果机器人在本地运行,并且任何机器人有应用 ID 和密码,则所有机器人都必须具有有效的 ID 和密码。
  • 如果所有机器人都已部署,请参阅如何使用 devtunnel 从任何通道调试机器人
  • 如果某些机器人在本地运行,并且部署了一些机器人,请参阅如何调试技能或技能使用者

或者,可以像调试其他机器人一样调试技能使用者或技能。 有关详细信息,请参阅调试机器人使用 Bot Framework Emulator 执行调试

其他信息