スキル コンシューマーを実装する

この記事の対象: SDK v4

スキルを使用して別のボットを拡張することができます。 "スキル" は別のボットに対して一連のタスクを実行できるボットであり、マニフェストを使用してそのインターフェイスが記述されます。 "ルート ボット" は、1 つ以上のスキルを呼び出すことができるユーザー向けのボットです。 ルート ボットは "スキル コンシューマー" の一種です。

  • スキル コンシューマーにアクセスできるスキルを管理するには、要求検証を使用する必要があります。
  • スキル コンシューマーは複数のスキルを使用できます。
  • スキルのソース コードにアクセスできない開発者は、スキル マニフェストの情報を使用してスキル コンシューマーを設計できます。

この記事では、エコー スキルを使用してユーザーの入力をエコーするスキル コンシューマーを実装する方法について説明します。 スキル マニフェストのサンプルおよびエコー スキルの実装に関する情報については、「スキルを実装する」を参照してください。

スキル ダイアログを使用してスキルを利用する方法については、ダイアログを使用してスキルを利用する方法を参照してください。

スキル コンシューマーの種類によっては、一部の種類のスキル ボットを使用できません。 次の表に、サポートされている組み合わせについて説明します。

  マルチテナント スキル シングルテナント スキル ユーザー割り当てマネージド ID スキル
マルチテナント コンシューマー サポートされています サポートされていません サポートされていません
シングルテナント コンシューマー サポートされていません 両方のアプリが同じテナントに属している場合にサポートされます 両方のアプリが同じテナントに属している場合にサポートされます
ユーザー割り当てマネージド ID コンシューマー サポートされていません 両方のアプリが同じテナントに属している場合にサポートされます 両方のアプリが同じテナントに属している場合にサポートされます

Note

Bot Framework JavaScript SDK、C#、Python SDK は引き続きサポートされますが、Java SDK については、最終的な長期サポートは 2023 年 11 月に終了する予定です。 このリポジトリ内の重要なセキュリティとバグの修正のみが行われます。

Java SDK を使用して構築された既存のボットは引き続き機能します。

新しいボットの構築については、Power Virtual Agents の使用を検討し、適切なチャットボット ソリューションの選択についてお読みください。

詳細については、「The future of bot building」をご覧ください。

前提条件

Note

バージョン 4.11 以降では、Bot Framework Emulator でスキル コンシューマーをローカルでテストするのにアプリ ID とパスワードは必要ありません。 コンシューマーを Azure にデプロイしたり、デプロイされたスキルを使用したりするには、引き続き Azure サブスクリプションが必要です。

このサンプルについて

skills simple bot-to-bot サンプルには、次の 2 つのボットのプロジェクトが含まれています。

  • スキルを実装する "エコー スキル ボット"。
  • スキルを使用するルート ボットを実装する "単純なルート ボット"。

この記事で取り上げるルート ボットには、ボット オブジェクトとアダプター オブジェクトにサポート ロジックが組み込まれ、かつアクティビティをスキルとやり取りするためのオブジェクトが含まれています。 これには以下が含まれます。

  • スキル クライアント。スキルにアクティビティを送信するために使用されます。
  • スキル ハンドラー。スキルからアクティビティを受信するために使用されます。
  • スキル会話 ID ファクトリ。ユーザー/ルート間の会話リファレンスの参照とルート/スキル間の会話リファレンスの間で変換を行うために、スキル クライアントとスキル ハンドラーで使用されます。

エコー スキル ボットについては、「スキルを実装する」を参照してください。

リソース

デプロイ済みのボットの場合、ボット間認証では、参加している各ボットが有効な ID 情報を保持している必要があります。 ただし、アプリ ID とパスワードを使用せずに、Emulator を使用してマルチテナント スキルとスキル コンシューマーをローカルでテストできます。

アプリケーション構成

  1. 必要に応じて、ルート ボットの ID 情報を構成ファイルに追加します。 スキルまたはスキル コンシューマーのいずれかで ID 情報を提供する場合は、両方で提供する必要があります。
  2. スキルがスキル コンシューマーに応答するスキル ホスト エンドポイント (サービスまたはコールバックURL) を追加します。
  3. スキル コンシューマーが使用するスキルごとにエントリを追加します。 各エントリには次のものが含まれます。
    • スキル コンシューマーが各スキルを識別するために使用する ID。
    • 必要に応じて、スキルのアプリ ID またはクライアント ID。
    • スキルのメッセージング エンドポイント。

Note

スキルまたはスキル コンシューマーのいずれかで ID 情報を提供する場合は、両方で提供する必要があります。

SimpleRootBot\appsettings.json

必要に応じて、ルート ボットの ID 情報を追加し、エコー スキル ボット用のアプリ ID またはクライアント ID を追加します。

{
  "MicrosoftAppType": "",
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",
  "MicrosoftAppTenantId": "",
  "SkillHostEndpoint": "http://localhost:3978/api/skills/",
  "BotFrameworkSkills": [
    {
      "Id": "EchoSkillBot",
      "AppId": "",
      "SkillEndpoint": "http://localhost:39783/api/messages"
    }
  ]
}

スキルの構成

このサンプルでは、構成ファイル内の各スキルの情報を、"スキル" オブジェクトのコレクションに読み込みます。

SimpleRootBot\SkillsConfiguration.cs

public class SkillsConfiguration
{
    public SkillsConfiguration(IConfiguration configuration)
    {
        var section = configuration?.GetSection("BotFrameworkSkills");
        var skills = section?.Get<BotFrameworkSkill[]>();
        if (skills != null)
        {
            foreach (var skill in skills)
            {
                Skills.Add(skill.Id, skill);
            }
        }

        var skillHostEndpoint = configuration?.GetValue<string>(nameof(SkillHostEndpoint));
        if (!string.IsNullOrWhiteSpace(skillHostEndpoint))
        {
            SkillHostEndpoint = new Uri(skillHostEndpoint);
        }
    }

    public Uri SkillHostEndpoint { get; }

    public Dictionary<string, BotFrameworkSkill> Skills { get; } = new Dictionary<string, BotFrameworkSkill>();
}

会話 ID ファクトリ

会話 ID ファクトリは、スキルで使用する会話 ID を作成し、スキルの会話 ID から元のユーザー会話 ID を復旧できるようにします。

このサンプルの会話 ID ファクトリは、次に示す単純なシナリオをサポートしています。

  • 1 つの特定のスキルを使用するようにルート ボットが設計されている。
  • ルート ボットとスキルの間で一度にアクティブになる会話は 1 つだけである。

SDK には、ソース コードのレプリケートを必要とせずに、任意のスキルで使用できる SkillConversationIdFactory クラスが用意されています。 会話 ID ファクトリは Startup.cs で構成されます。

より複雑なシナリオをサポートするには、次のように会話 ID ファクトリを設計します。

  • create skill conversation ID メソッドで、適切なスキル会話 ID を取得または生成します。
  • get conversation reference メソッドで、正しいユーザー会話を取得します。

スキル クライアントとスキル ハンドラー

スキル コンシューマーは、スキル クライアントを使用してアクティビティをスキルに転送します。 クライアントは、このためにスキルの構成情報と会話 ID ファクトリを使用します。

スキル コンシューマーは、スキル ハンドラーを使用してスキルからアクティビティを受信します。 ハンドラーは、このために会話 ID ファクトリ、認証構成、および資格情報プロバイダーを使用するほか、ルート ボットのアダプターとアクティビティ ハンドラーへの依存関係を持ちます。

SimpleRootBot\Startup.cs

services.AddSingleton<IBotFrameworkHttpAdapter>(sp => sp.GetService<CloudAdapter>());
services.AddSingleton<BotAdapter>(sp => sp.GetService<CloudAdapter>());

スキルからの HTTP トラフィックは、スキル コンシューマーがスキルにアドバタイズするサービス URL エンドポイントに送られます。 トラフィックをスキル ハンドラーに転送するには、言語固有のエンドポイント ハンドラーを使用します。

既定のスキル ハンドラーは次のことを行います。

  • アプリ ID とパスワードが存在する場合は、認証構成オブジェクトを使用して、ボット間認証と要求検証の両方を実行します。
  • 会話 ID ファクトリを使用して、コンシューマー/スキル間の会話を、元のルート/ユーザー間の会話に変換します。
  • プロアクティブ メッセージを生成して、スキル コンシューマーがルート/ユーザー間のターン コンテキストを再確立し、アクティビティをユーザーに転送できるようにします。

アクティビティ ハンドラーのロジック

注目すべき点として、スキル コンシューマーのロジックでは次の処理を行う必要があります。

  • アクティブなスキルの有無を記憶し、必要に応じてそれらのスキルにアクティビティに転送します。
  • スキルへの転送が必要な要求をユーザーが行ったことを確認し、スキルを開始します。
  • アクティブなスキルからの endOfConversation アクティビティを探し、その完了を確認します。
  • 必要に応じて、未完了のスキルをユーザーまたはスキル コンシューマーがキャンセルできるようにするロジックを追加します。
  • 応答がスキル コンシューマーの別のインスタンスに返される可能性があるため、スキルを呼び出す前に状態を保存します。

SimpleRootBot\Bots\RootBot.cs

ルート ボットには、会話状態、スキル情報、スキル クライアント、および全般的な構成への依存関係があります。 これらのオブジェクトは、ASP.NET から依存関係の挿入を通じて提供されます。 またルート ボットでは、アクティブなスキルを追跡するための会話状態プロパティ アクセサーが定義されます。

public static readonly string ActiveSkillPropertyName = $"{typeof(RootBot).FullName}.ActiveSkillProperty";
private readonly IStatePropertyAccessor<BotFrameworkSkill> _activeSkillProperty;
private readonly string _botId;
private readonly ConversationState _conversationState;
private readonly BotFrameworkAuthentication _auth;
private readonly SkillConversationIdFactoryBase _conversationIdFactory;
private readonly SkillsConfiguration _skillsConfig;
private readonly BotFrameworkSkill _targetSkill;

public RootBot(BotFrameworkAuthentication auth, ConversationState conversationState, SkillsConfiguration skillsConfig, SkillConversationIdFactoryBase conversationIdFactory, IConfiguration configuration)
{
    _auth = auth ?? throw new ArgumentNullException(nameof(auth));
    _conversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
    _skillsConfig = skillsConfig ?? throw new ArgumentNullException(nameof(skillsConfig));
    _conversationIdFactory = conversationIdFactory ?? throw new ArgumentNullException(nameof(conversationIdFactory));

    if (configuration == null)
    {
        throw new ArgumentNullException(nameof(configuration));
    }

    _botId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value;

    // We use a single skill in this example.
    var targetSkillId = "EchoSkillBot";
    _skillsConfig.Skills.TryGetValue(targetSkillId, out _targetSkill);

    // Create state property to track the active skill
    _activeSkillProperty = conversationState.CreateProperty<BotFrameworkSkill>(ActiveSkillPropertyName);
}

次のサンプルでは、アクティビティをスキルに転送するためのヘルパー メソッドが使用されています。 このメソッドは、スキルを呼び出す前に会話状態を保存し、HTTP 要求が成功したかどうかを確認します。

private async Task SendToSkill(ITurnContext turnContext, BotFrameworkSkill targetSkill, CancellationToken cancellationToken)
{
    // NOTE: Always SaveChanges() before calling a skill so that any activity generated by the skill
    // will have access to current accurate state.
    await _conversationState.SaveChangesAsync(turnContext, force: true, cancellationToken: cancellationToken);

    // Create a conversationId to interact with the skill and send the activity
    var options = new SkillConversationIdFactoryOptions
    {
        FromBotOAuthScope = turnContext.TurnState.Get<string>(BotAdapter.OAuthScopeKey),
        FromBotId = _botId,
        Activity = turnContext.Activity,
        BotFrameworkSkill = targetSkill
    };
    var skillConversationId = await _conversationIdFactory.CreateSkillConversationIdAsync(options, cancellationToken);

    using var client = _auth.CreateBotFrameworkClient();

    // route the activity to the skill
    var response = await client.PostActivityAsync(_botId, targetSkill.AppId, targetSkill.SkillEndpoint, _skillsConfig.SkillHostEndpoint, skillConversationId, turnContext.Activity, cancellationToken);

    // Check response status
    if (!(response.Status >= 200 && response.Status <= 299))
    {
        throw new HttpRequestException($"Error invoking the skill id: \"{targetSkill.Id}\" at \"{targetSkill.SkillEndpoint}\" (status is {response.Status}). \r\n {response.Body}");
    }
}

注目すべき点として、ルート ボットには、アクティビティのスキルへの転送、ユーザーの要求でのスキルの開始、およびスキルが完了したときのスキルの停止を行うためのロジックが含まれています。

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    if (turnContext.Activity.Text.Contains("skill"))
    {
        await turnContext.SendActivityAsync(MessageFactory.Text("Got it, connecting you to the skill..."), cancellationToken);

        // Save active skill in state
        await _activeSkillProperty.SetAsync(turnContext, _targetSkill, cancellationToken);

        // Send the activity to the skill
        await SendToSkill(turnContext, _targetSkill, cancellationToken);
        return;
    }

    // just respond
    await turnContext.SendActivityAsync(MessageFactory.Text("Me no nothin'. Say \"skill\" and I'll patch you through"), cancellationToken);

    // Save conversation state
    await _conversationState.SaveChangesAsync(turnContext, force: true, cancellationToken: cancellationToken);
}

protected override async Task OnEndOfConversationActivityAsync(ITurnContext<IEndOfConversationActivity> turnContext, CancellationToken cancellationToken)
{
    // forget skill invocation
    await _activeSkillProperty.DeleteAsync(turnContext, cancellationToken);

    // Show status message, text and value returned by the skill
    var eocActivityMessage = $"Received {ActivityTypes.EndOfConversation}.\n\nCode: {turnContext.Activity.Code}";
    if (!string.IsNullOrWhiteSpace(turnContext.Activity.Text))
    {
        eocActivityMessage += $"\n\nText: {turnContext.Activity.Text}";
    }

    if ((turnContext.Activity as Activity)?.Value != null)
    {
        eocActivityMessage += $"\n\nValue: {JsonConvert.SerializeObject((turnContext.Activity as Activity)?.Value)}";
    }

    await turnContext.SendActivityAsync(MessageFactory.Text(eocActivityMessage), cancellationToken);

    // We are back at the root
    await turnContext.SendActivityAsync(MessageFactory.Text("Back in the root bot. Say \"skill\" and I'll patch you through"), cancellationToken);

    // Save conversation state
    await _conversationState.SaveChangesAsync(turnContext, cancellationToken: cancellationToken);
}

オン ターン エラー ハンドラー

エラーが発生すると、アダプターは会話状態をクリアしてユーザーとの会話をリセットし、エラー状態を解消します。

スキル コンシューマーで会話状態をクリアする前に、アクティブなスキルに 会話終了 アクティビティを送信することをお勧めします。 これにより、スキル コンシューマーが会話を解放する前に、コンシューマ/スキル間の会話に関連するリソースをスキルが解放できるようになります。

SimpleRootBot\AdapterWithErrorHandler.cs

このサンプルでは、ターン エラー ロジックがいくつかのヘルパー メソッドに分けられています。

private async Task HandleTurnError(ITurnContext turnContext, Exception exception)
{
    // Log any leaked exception from the application.
    // NOTE: In production environment, you should consider logging this to
    // Azure Application Insights. Visit https://aka.ms/bottelemetry to see how
    // to add telemetry capture to your bot.
    _logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");

    await SendErrorMessageAsync(turnContext, exception);
    await EndSkillConversationAsync(turnContext);
    await ClearConversationStateAsync(turnContext);
}

private async Task SendErrorMessageAsync(ITurnContext turnContext, Exception exception)
{
    try
    {
        // Send a message to the user
        var errorMessageText = "The bot encountered an error or bug.";
        var errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.IgnoringInput);
        await turnContext.SendActivityAsync(errorMessage);

        errorMessageText = "To continue to run this bot, please fix the bot source code.";
        errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.ExpectingInput);
        await turnContext.SendActivityAsync(errorMessage);

        // Send a trace activity, which will be displayed in the Bot Framework Emulator
        await turnContext.TraceActivityAsync("OnTurnError Trace", exception.ToString(), "https://www.botframework.com/schemas/error", "TurnError");
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, $"Exception caught in SendErrorMessageAsync : {ex}");
    }
}

private async Task EndSkillConversationAsync(ITurnContext turnContext)
{
    if (_skillsConfig == null)
    {
        return;
    }

    try
    {
        // Inform the active skill that the conversation is ended so that it has
        // a chance to clean up.
        // Note: ActiveSkillPropertyName is set by the RooBot while messages are being
        // forwarded to a Skill.
        var activeSkill = await _conversationState.CreateProperty<BotFrameworkSkill>(RootBot.ActiveSkillPropertyName).GetAsync(turnContext, () => null);
        if (activeSkill != null)
        {
            var botId = _configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value;

            var endOfConversation = Activity.CreateEndOfConversationActivity();
            endOfConversation.Code = "RootSkillError";
            endOfConversation.ApplyConversationReference(turnContext.Activity.GetConversationReference(), true);

            await _conversationState.SaveChangesAsync(turnContext, true);

            using var client = _auth.CreateBotFrameworkClient();

            await client.PostActivityAsync(botId, activeSkill.AppId, activeSkill.SkillEndpoint, _skillsConfig.SkillHostEndpoint, endOfConversation.Conversation.Id, (Activity)endOfConversation, CancellationToken.None);
        }
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, $"Exception caught on attempting to send EndOfConversation : {ex}");
    }
}

private async Task ClearConversationStateAsync(ITurnContext turnContext)
{
    try
    {
        // Delete the conversationState for the current conversation to prevent the
        // bot from getting stuck in a error-loop caused by being in a bad state.
        // ConversationState should be thought of as similar to "cookie-state" in a Web pages.
        await _conversationState.DeleteAsync(turnContext);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, $"Exception caught on attempting to Delete ConversationState : {ex}");
    }
}

スキル エンドポイント

ボットでは、受信したスキル アクティビティをルート ボットのスキル ハンドラーに転送するエンドポイントが定義されます。

SimpleRootBot\Controllers\SkillController.cs

[ApiController]
[Route("api/skills")]
public class SkillController : ChannelServiceController
{
    public SkillController(ChannelServiceHandlerBase handler)
        : base(handler)
    {
    }
}

サービス登録

要求検証を含む認証構成オブジェクトを、すべての追加オブジェクトと共に組み込みます。 このサンプルでは、ユーザーとスキルの両方からのアクティビティを検証するのに、同じ認証構成ロジックを使用します。

SimpleRootBot\Startup.cs

// Register the skills configuration class
services.AddSingleton<SkillsConfiguration>();

// Register AuthConfiguration to enable custom claim validation.
services.AddSingleton(sp =>
{
    var allowedSkills = sp.GetService<SkillsConfiguration>().Skills.Values.Select(s => s.AppId).ToList();

    var claimsValidator = new AllowedSkillsClaimsValidator(allowedSkills);

    // If TenantId is specified in config, add the tenant as a valid JWT token issuer for Bot to Skill conversation.
    // The token issuer for MSI and single tenant scenarios will be the tenant where the bot is registered.
    var validTokenIssuers = new List<string>();
    var tenantId = sp.GetService<IConfiguration>().GetSection(MicrosoftAppCredentials.MicrosoftAppTenantIdKey)?.Value;

    if (!string.IsNullOrWhiteSpace(tenantId))
    {
        // For SingleTenant/MSI auth, the JWT tokens will be issued from the bot's home tenant.
        // Therefore, these issuers need to be added to the list of valid token issuers for authenticating activity requests.
        validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, tenantId));
        validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, tenantId));
        validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV1, tenantId));
        validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV2, tenantId));
    }

    return new AuthenticationConfiguration
    {
        ClaimsValidator = claimsValidator,
        ValidTokenIssuers = validTokenIssuers
    };
});

ルート ボットのテスト

スキル コンシューマーは通常のボットと同様にエミュレーターでテストできますが、スキルとスキル コンシューマーの両方のボットを同時に実行する必要があります。 スキルを構成する方法については、「スキルを実装する」を参照してください。

最新の Bot Framework Emulator をダウンロードしてインストールします

  1. エコー スキル ボットと単純なルート ボットをお使いのマシンでローカルに実行します。 手順が必要な場合は、README ファイルで C#JavaScriptJava、または Python のサンプルを参照してください。
  2. 次に示すように、エミュレーターを使用してボットをテストします。 スキルに end または stop メッセージを送信すると、スキルからルート ボットに対し、応答メッセージに加えて endOfConversation アクティビティが送信されます。 endOfConversation アクティビティの code プロパティにより、スキルが正常に完了したことが示されます。

Example transcript of an interaction with the skill consumer.

デバッグの詳細

スキルとスキル コンシューマーの間のトラフィックは認証されるため、このようなボットをデバッグする際には追加の手順があります。

  • スキル コンシューマーおよびそれが使用するすべてのスキルが、直接または間接的に実行されている必要があります。
  • ボットがローカルで実行されており、いずれかのボットにアプリ ID とパスワードが設定されている場合は、すべてのボットに有効な ID とパスワードが必要です。
  • ボットがすべてデプロイ済みである場合は、ngrok を使用して任意のチャネルからボットをデバッグする方法をご覧ください。
  • 一部のボットがローカルで実行されており、一部がデプロイ済みである場合は、スキルまたはスキル コンシューマーをデバッグする方法に関する記事をご覧ください。

それ以外の場合は、その他のボットをデバッグするのと同様に、スキル コンシューマーまたはスキルをデバッグできます。 詳細については、ボットのデバッグに関する記事、およびBot Framework Emulator を使用したデバッグに関する記事を参照してください。

追加情報

より複雑なルート ボットを実装する場合に検討すべき事項を次に示します。

ユーザーが複数ステップのスキルをキャンセルできるようにする

ルート ボットでは、ユーザーのメッセージをアクティブなスキルに転送する前に確認する必要があります。 ユーザーが現在のプロセスのキャンセルを求めた場合、ルート ボットはメッセージを転送する代わりに endOfConversation アクティビティをスキルに送信できます。

ルート ボットとスキル ボットの間でデータをやり取りする

スキル コンシューマーでは、スキルにパラメーターを送信するために、スキルに送信するメッセージに value プロパティを設定できます。 スキルから戻り値を受信するために、スキル コンシューマーでは、スキルから endOfConversation アクティビティが送信されたときに value プロパティを確認する必要があります。

複数のスキルを使用する

  • アクティブなスキルがある場合、ルート ボットはそのスキルを特定し、ユーザーのメッセージを適切なスキルに転送する必要があります。
  • アクティブなスキルがない場合、ルート ボットは、ボットの状態とユーザーの入力を基に、開始するスキル (存在する場合) を特定する必要があります。
  • ユーザーが複数の同時スキル間を切り替えられるようにする場合は、ルート ボットで、ユーザーのメッセージを転送する前にユーザーが対話しようとしているアクティブなスキルを特定する必要があります。

予想される応答の配信モードを使用するには

予想される応答の配信モードを使用するには:

  • ターン コンテキストからアクティビティを複製します。
  • ルート ボットからスキルにアクティビティを送信する前に、新しいアクティビティの配信モードプロパティを "ExpectReplies" に設定します。
  • 予想される応答を、要求応答から返された呼び出し応答の本文から読み取ります。
  • ルート ボット内で、または元の要求を開始したチャネルに送信して、各アクティビティを処理します。

予想される応答は、アクティビティに応答するボットが、アクティビティを受信したボットの同じインスタンスである必要がある場合に役立ちます。