Implémenter un consommateur de compétences

S'APPLIQUE À : SDK v4

Vous pouvez utiliser des compétences pour étendre un autre bot. Une compétence est un bot qui peut effectuer un ensemble de tâches pour un autre bot et qui utilise un manifeste pour décrire son interface. Un bot racine est un bot orienté utilisateur qui peut appeler une ou plusieurs compétences. Un bot racine est un type de consommateur de compétences.

  • Un consommateur de compétences peut utiliser la validation des revendications pour gérer les compétences ou les utilisateurs qui peuvent y accéder.
  • Un consommateur de compétences peut utiliser plusieurs compétences.
  • Les développeurs qui n’ont pas accès au code source de la compétence peuvent utiliser les informations contenues dans le manifeste de la compétence pour concevoir leur consommateur de compétences.

Cet article montre comment implémenter un consommateur de compétences qui utilise la compétence echo pour reprendre l’entrée de l’utilisateur. Pour obtenir un exemple de manifeste de compétence et des informations sur l’implémentation de la compétence echo, découvrez comment implémenter une compétence.

Pour plus d’informations sur l’utilisation d’un dialogue de compétence pour consommer une compétence, consultez Utiliser un dialogue pour consommer une compétence.

Certains types de consommateurs de compétences ne peuvent pas utiliser certains types de bots de compétence. Le tableau suivant décrit les combinaisons prises en charge.

  Compétence multilocataire Compétence monolocataire Compétence en matière d'identité managée affectée par l'utilisateur
Consommateur multilocataire Prise en charge Non pris en charge Non pris en charge
Consommateur d'un locataire unique Non pris en charge Pris en charge, si les deux applications appartiennent au même locataire Pris en charge, si les deux applications appartiennent au même locataire
Consommateur d'identité managée affectée par l'utilisateur Non pris en charge Pris en charge, si les deux applications appartiennent au même locataire Pris en charge, si les deux applications appartiennent au même locataire

Remarque

Les kits de développement logiciel (SDK) JavaScript, C# et Python bot Framework continueront d'être pris en charge. Toutefois, le kit de développement logiciel (SDK) Java est mis hors service avec une prise en charge finale à long terme se terminant en novembre 2023. Seuls les correctifs de sécurité et de bogues critiques dans ce référentiel seront appliqués.

Les bots existants créés avec le kit de développement logiciel (SDK) Java continueront de fonctionner.

Pour la nouvelle génération de bots, envisagez d'utiliser Power Virtual Agents et découvrez comment choisir la solution de chatbot appropriée.

Pour plus d'informations, consultez Les futures versions de bot.

Prérequis

Remarque

À compter de la version 4.11, vous n'avez pas besoin d'un ID d'application et d'un mot de passe pour tester un consommateur de compétences localement dans le Bot Framework Emulator. Un abonnement Azure est toujours nécessaire pour déployer votre consommateur sur Azure ou pour consommer une compétence déployée.

À propos de cet exemple

L’exemple de bot-à-bot simple de compétences comprend des projets pour deux bots :

  • Bot de compétences echo, qui implémente la compétence.
  • Bot racine simple, qui implémente un bot racine qui consomme la compétence.

Cet article se concentre sur le bot racine, qui inclut la logique de prise en charge dans ses objets de bot et d’adaptateur, et comprend des objets utilisés pour échanger des activités avec une compétence. Il s’agit notamment des paramètres suivants :

  • Un client de compétence, utilisé pour envoyer des activités à une compétence.
  • Un gestionnaire de compétence, utilisé pour recevoir des activités d’une compétence.
  • Une fabrique d’ID de conversation de compétence, utilisée par le client et le gestionnaire de compétence pour effectuer une conversion entre la référence de la conversation utilisateur-racine et la référence de la conversation racine-compétence.

Pour plus d’informations sur le bot de compétences echo, découvrez comment implémenter une compétence.

Ressources

Pour les bots déployés, l'authentification entre bots exige que chaque bot participant ait une identité valide. Toutefois, vous pouvez tester les compétences multilocataires et les consommateurs de compétences localement avec l'émulateur sans ID d'application et mot de passe.

Configuration de l’application

  1. De manière optionnelle, ajoutez les informations relatives à l'identité du bot racine à son fichier config. Si la compétence ou le consommateur de compétences fournit des informations d'identité, les deux le doivent.
  2. Ajoutez le point de terminaison de l'hôte des compétences (le service ou l'URL de rappel) auquel les compétences doivent répondre au consommateur de compétences.
  3. Ajoutez une entrée pour chaque compétence que le consommateur de compétences utilisera. Chaque entrée comprend :
    • Un ID que le consommateur de compétences utilisera pour identifier chaque compétence.
    • De manière optionnelle, l'application de compétence ou l'ID client.
    • Le point de terminaison de messagerie de la compétence.

Remarque

Si la compétence ou le consommateur de compétences fournit des informations d'identité, les deux le doivent.

SimpleRootBot\appsettings.json

Si vous le souhaitez, ajoutez les informations d'identité du bot racine et l'application ou l'ID client pour le bot de compétence écho.

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

Configuration des compétences

Cet exemple lit des informations pour chaque compétence dans le fichier de configuration dans une collection d’objets de compétence.

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>();
}

Fabrique d’ID de conversation

Elle crée l’ID de conversation à utiliser avec la compétence et peut récupérer l’ID de conversation de l’utilisateur d’origine à partir de l’ID de conversation de la compétence.

La fabrique d’ID de conversation pour cet exemple prend en charge un scénario simple dans lequel :

  • Le bot racine est conçu pour consommer une compétence spécifique.
  • Le bot racine n’a qu’une seule conversation active avec une compétence à la fois.

Le kit de développement logiciel (SDK) fournit une classe SkillConversationIdFactory qui peut être utilisée dans toutes les compétences sans qu'il soit nécessaire de reproduire le code source. La fabrique d'ID de conversation est configurée dans Startup.cs.

Pour prendre en charge des scénarios plus complexes, concevez votre fabrique d’ID de conversation de sorte que :

  • La méthode de création de l’ID de conversation de compétence obtienne ou génère l’ID de conversation de compétence approprié.
  • La méthode d’obtention de la référence de conversation obtienne la conversation d’utilisateur appropriée.

Client de compétence et gestionnaire de compétence

Le consommateur de compétences utilise un client de compétence pour transférer des activités à la compétence. Pour ce faire, le client utilise les informations de configuration des compétences et la fabrique d’ID de conversation.

Le consommateur de compétences utilise un gestionnaire de compétence pour recevoir des activités d’une compétence. Le gestionnaire utilise la fabrique d’ID de conversation, la configuration d’authentification et un fournisseur d’informations d’identification à cet effet. Il a également des dépendances sur l’adaptateur du bot racine et le gestionnaire d’activités

SimpleRootBot\Startup.cs

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

Le trafic HTTP de la compétence apparaît dans le point de terminaison de l'URL de service que le consommateur de compétences annonce à la compétence. Utilisez un gestionnaire de point de terminaison spécifique au langage pour transférer le trafic vers le gestionnaire de compétence.

Le gestionnaire de compétence par défaut :

  • En présence d'un ID d'application et d'un mot de passe, il utilise un objet de configuration d'authentification pour effectuer l'authentification de bot à bot et la validation des réclamations.
  • Utilise la fabrique d’ID de conversation pour effectuer une conversion de la conversation consommateur-compétence en conversation racine-utilisateur.
  • Génère un message proactif afin que le consommateur de compétences puisse rétablir un contexte de tour racine-utilisateur et transférer des activités à l’utilisateur.

Logique du gestionnaire d’activités

Notez que la logique du consommateur de compétences doit :

  • Mémoriser s’il existe des compétences actives et leur transférer des activités si nécessaire.
  • Noter quand un utilisateur effectue une demande qui doit être transférée à une compétence, puis démarrer la compétence.
  • Rechercher une activité endOfConversation à partir de n’importe quelle compétence active pour noter lorsqu’elle se termine.
  • Le cas échéant, ajouter une logique pour permettre à l’utilisateur ou au consommateur de compétences d’annuler une compétence qui n’est pas encore terminée.
  • Enregistrer l’état avant d’effectuer l’appel à une compétence, car toute réponse peut revenir à une autre instance du consommateur de compétences

SimpleRootBot\Bots\RootBot.cs

Le bot racine a des dépendances sur l’état de la conversation, les informations sur les compétences, le client de compétence et la configuration générale. ASP.NET fournit ces objets par le biais de l’injection de dépendances. Le bot racine définit également un accesseur de propriété d’état de conversation pour savoir quelle compétence est active.

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);
}

Cet exemple contient une méthode d’assistance pour transférer des activités à une compétence. Cette méthode enregistre l’état de la conversation avant d’appeler la compétence et vérifie si la requête HTTP a réussi.

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}");
    }
}

Notez que le bot racine comprend la logique permettant de transférer les activités vers la compétence, de démarrer la compétence à la demande de l’utilisateur et d’arrêter la compétence lorsque celle-ci est terminée.

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);
}

Gestionnaire d’erreurs de tour

Lorsqu’une erreur se produit, l’adaptateur efface l’état de la conversation pour réinitialiser la conversation avec l’utilisateur et éviter la persistance d’un état d’erreur.

Il est recommandé d'envoyer une activité présentant la fin de conversation à toute compétence active avant de supprimer l'état de la conversation dans le consommateur de compétences. Cela permet à la compétence de libérer toutes les ressources associées à la conversation consommateur-compétence avant que le consommateur de compétences ne libère la conversation.

SimpleRootBot\AdapterWithErrorHandler.cs

Dans cet échantillon, la logique d'erreur de tour est divisée en plusieurs méthodes d'assistance.

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}");
    }
}

Point de terminaison de compétences

Le bot définit un point de terminaison qui transfère les activités de compétences entrantes au gestionnaire de compétence du bot racine.

SimpleRootBot\Controllers\SkillController.cs

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

Inscription du service

Incluez un objet de configuration d’authentification avec une validation des revendications ainsi que tous les objets supplémentaires. Cet échantillon utilise la même logique de configuration d'authentification pour valider les activités des utilisateurs et des compétences.

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
    };
});

Tester le bot racine

Vous pouvez tester le consommateur de compétences dans l’émulateur comme s’il s’agissait d’un bot normal. Toutefois, vous devez exécuter les bots de compétences et de consommateurs de compétences en même temps. Pour plus d’informations sur la configuration de la compétence, découvrez comment implémenter une compétence.

Téléchargez et installez la dernière version de Bot Framework Emulator.

  1. Exécutez le bot de compétences echo et le bot racine simple localement sur votre ordinateur. Si vous avez besoin d'instructions, reportez-vous au fichier README de l'échantillon C#, JavaScript, Java ou Python.
  2. Utilisez l’émulateur pour tester le bot comme indiqué ci-dessous. Lorsque vous envoyez un message end ou stop à la compétence, elle envoie au bot racine une activité endOfConversation, en plus du message de réponse. La propriété code de l’activité endOfConversation indique que la compétence a été correctement exécutée.

Example transcript of an interaction with the skill consumer.

Plus d'informations sur le débogage

Étant donné que le trafic entre les compétences et les consommateurs de compétences est authentifié, des étapes supplémentaires sont nécessaires pour déboguer ces bots.

Dans le cas contraire, vous pouvez déboguer un consommateur de compétences ou une compétence comme vous déboguez d'autres bots. Pour plus d'informations, consultez Débogage d'un bot et Débogage avec Bot Framework Emulator.

Informations supplémentaires

Voici quelques éléments à prendre en compte lors de l’implémentation d’un bot racine plus complexe.

Pour permettre à l’utilisateur d’annuler une compétence en plusieurs étapes

Le bot racine doit vérifier le message de l’utilisateur avant de le transférer à la compétence active. Si l’utilisateur souhaite annuler le processus en cours, le bot racine peut envoyer une activité endOfConversation à la compétence, au lieu de transférer le message.

Pour échanger des données entre les bots racine et de compétences

Pour envoyer des paramètres à la compétence, le consommateur de compétences peut définir la propriété value sur les messages qu’il envoie à la compétence. Pour recevoir des valeurs de retour de la compétence, le consommateur de compétences doit vérifier la propriété value lorsque la compétence envoie une activité endOfConversation.

Pour utiliser plusieurs compétences

  • Si une compétence est active, le bot racine doit identifier la compétence qui est active et transférer le message de l’utilisateur à la compétence appropriée.
  • Si aucune compétence n’est active, le bot racine doit identifier la compétence à démarrer, le cas échéant, en fonction de l’état du bot et de l’entrée de l’utilisateur.
  • Si vous souhaitez autoriser l’utilisateur à basculer entre plusieurs compétences simultanées, le bot racine doit identifier les compétences actives avec lesquelles l’utilisateur a l’intention d’interagir avant de transférer le message de l’utilisateur.

Pour utiliser un mode de délivrance des réponses attendues

Pour utiliser le mode de délivrance des réponses attendues :

  • Clonez l'activité à partir du contexte de tour.
  • Définissez la propriété du mode de délivrance de la nouvelle activité sur « ExpectReplies » avant d'envoyer l'activité du bot racine à la compétence.
  • Consultez les réponses attendues à partir du corps de la réponse d'invocation retournée par la requête-réponse.
  • Traitez chaque activité, soit dans le bot racine, soit en l'envoyant à la chaîne qui a lancé la demande d'origine.

Les réponses attendues peuvent être utiles dans les situations où le bot qui répond à une activité doit être la même instance du bot qui a reçu l'activité.