Implementare un consumer di competenze

SI APPLICA A: SDK v4

È possibile usare le competenze per estendere un altro bot. Una competenza è un bot che può eseguire un set di attività per un altro bot e usa un manifesto per descrivere la propria interfaccia. Un bot radice è un bot rivolto all'utente che può richiamare una o più competenze. Un bot radice è un tipo di consumer di competenze.

  • Un consumer di competenze deve usare la convalida delle attestazioni per gestire le competenze che possono accedervi.
  • Un consumer di competenze può usare più competenze.
  • Gli sviluppatori che non hanno accesso al codice sorgente della competenza possono usare le informazioni nel manifesto delle competenze per progettare il proprio consumer di competenze.

Questo articolo illustra come implementare un consumer di competenze che usa la competenza echo per restituisce l'input dell'utente. Per un manifesto di competenza di esempio e per informazioni sull'implementazione della competenza echo, vedere Implementare una competenza.

Per informazioni sull'uso di un dialogo per una competenza, vedere Usare un dialogo per una competenza.

Alcuni tipi di consumer di competenze non sono in grado di usare alcuni tipi di bot di competenza. Nella tabella seguente vengono descritte le combinazioni supportate.

  Competenza multi-tenant Competenza a tenant singolo Competenza identità gestita assegnata dall'utente
Consumer multi-tenant Supportato Non supportato Non supportato
Consumer a tenant singolo Non supportato Supportato se entrambe le app appartengono allo stesso tenant Supportato se entrambe le app appartengono allo stesso tenant
Consumer dell'identità gestita assegnata dall'utente Non supportato Supportato se entrambe le app appartengono allo stesso tenant Supportato se entrambe le app appartengono allo stesso tenant

Nota

Gli SDK JavaScript, C# e Python di Bot Framework continueranno a essere supportati, ma Java SDK verrà ritirato con il supporto finale a lungo termine che termina a novembre 2023.

I bot esistenti creati con Java SDK continueranno a funzionare.

Per la creazione di nuovi bot, prendere in considerazione l'uso di Power Virtual Agents e leggere la scelta della soluzione chatbot appropriata.

Per altre informazioni, vedere Il futuro della compilazione di bot.

Prerequisiti

Nota

A partire dalla versione 4.11, non è necessario un ID app e una password per testare un consumer di competenze in locale in Bot Framework Emulator. Una sottoscrizione di Azure è comunque necessaria per distribuire il consumer in Azure o per usare una competenza distribuita.

Informazioni sull'esempio

L'esempio di competenza semplice da bot a bot include i progetti per due bot:

  • Il bot di competenza echo, che implementa la competenza.
  • Il bot radice semplice, che implementa un bot radice che utilizza la competenza.

Questo articolo è incentrato sul bot radice, che include la logica di supporto nei relativi oggetti bot e adapter, nonché gli oggetti usati per lo scambio di attività con una competenza, tra cui:

  • Un client di competenze, usato per inviare attività a una competenza.
  • Un gestore di competenze, usato per ricevere attività da una competenza.
  • Factory dell'ID conversazione della competenza, usata dal client e dal gestore di competenze per la conversione tra il riferimento alla conversazione tra utente e radice e il riferimento alla conversazione tra radice e competenza.

Per informazioni sul bot di competenza echo, vedere Implementare una competenza.

Risorse

Per i bot distribuiti, l'autenticazione da bot a bot richiede che ogni bot partecipante disponga di informazioni di identità valide. Tuttavia, è possibile testare le competenze e i consumer di competenze multi-tenant in locale con l'emulatore senza un ID app e una password.

Configurazione dell'applicazione

  1. Facoltativamente, aggiungere le informazioni sull'identità del bot radice al file di configurazione. Se la competenza o il consumer di competenze fornisce informazioni sull'identità, entrambe devono.
  2. Aggiungere l'endpoint host della competenza (l'URL del servizio o di callback) a cui le competenze devono rispondere al consumer di competenze.
  3. Aggiungere una voce per ogni competenza usata dal consumer di competenze. Ogni voce include:
    • ID usato dal consumer di competenze per identificare le singole competenze.
    • Facoltativamente, l'app o l'ID client della competenza.
    • Endpoint di messaggistica della competenza.

Nota

Se la competenza o il consumer di competenze fornisce informazioni sull'identità, entrambe devono.

SimpleRootBot\appsettings.json

Facoltativamente, aggiungere le informazioni sull'identità del bot radice e aggiungere l'app o l'ID client per il bot di competenza echo.

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

Configurazione delle competenze

In questo esempio le informazioni relative alle singole competenze nel file di configurazione vengono lette in una raccolta di oggetti competenza.

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

Factory dell'ID conversazione

Questo elemento consente di creare l'ID conversazione da usare con la competenza e di recuperare l'ID conversazione utente originale dall'ID conversazione delle competenze.

La factory dell'ID conversazione in questo esempio supporta uno scenario semplice in cui:

  • Il bot radice è progettato per utilizzare un'unica competenza specifica.
  • Il bot radice include una sola conversazione attiva con una competenza alla volta.

L'SDK fornisce una SkillConversationIdFactory classe che può essere usata in qualsiasi competenza senza richiedere la replica del codice sorgente. La factory dell'ID conversazione è configurata in Startup.cs.

Per supportare scenari più complessi, progettare la factory dell'ID conversazione in modo che:

  • Il metodo CreateSkillConversationId ottenga o generi l'ID conversazione della competenza appropriato.
  • Il metodo GetConversationReference ottenga la conversazione utente corretta.

Client di competenze e gestore di competenze

Il consumer di competenze usa un client di competenze per inoltrare attività alla competenza. A tale scopo, il client usa le informazioni di configurazione delle competenze e la factory dell'ID conversazione.

Il consumer di competenze usa un gestore di competenze per ricevere attività da una competenza. A tale scopo, il gestore usa la factory dell'ID conversazione, la configurazione di autenticazione e un provider di credenziali e include anche le dipendenze dall'adapter e dal gestore di attività del bot radice.

SimpleRootBot\Startup.cs

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

Il traffico HTTP dalla competenza entra nell'endpoint URL del servizio che il consumer di competenze annuncia alla competenza. Usare un gestore di endpoint specifico del linguaggio per l'inoltro del traffico al gestore di competenze.

Il gestore di competenze predefinito:

  • Se sono presenti un ID app e una password, usa un oggetto di configurazione di autenticazione per eseguire sia l'autenticazione da bot a bot che la convalida delle attestazioni.
  • Usa la factory dell'ID conversazione per eseguire di nuovo la conversione dalla conversazione tra consumer e competenza alla conversazione tra radice e utente.
  • Genera un messaggio proattivo, in modo che il consumer di competenze possa ristabilire un contesto di turno tra radice e utente e inoltrare le attività all'utente.

Logica del gestore di attività

Si noti che la logica del consumer di competenze deve:

  • Ricordare se sono presenti competenze attive e, se necessario, inoltrare loro le attività.
  • Notare quando un utente effettua una richiesta che deve essere inoltrata a una competenza e avviare la competenza.
  • Cercare un'attività endOfConversation proveniente da qualsiasi competenza attiva, per notare quando viene completata.
  • Se necessario, aggiungere la logica per consentire all'utente o al consumer di competenze di annullare una competenza non ancora completata.
  • Salvare lo stato prima di effettuare la chiamata a una competenza, in quanto qualsiasi risposta potrebbe tornare a un'istanza diversa del consumer di competenze

SimpleRootBot\Bots\RootBot.cs

Il bot radice include dipendenze sullo stato della conversazione, sulle informazioni sulle competenze, sul client di competenze e sulla configurazione generale. ASP.NET fornisce questi oggetti tramite l'inserimento di dipendenze. Il bot radice definisce anche una funzione di accesso della proprietà dello stato di conversazione per tenere traccia della competenza attiva.

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

Questo esempio include un metodo helper per l'inoltro di attività a una competenza. Salva lo stato della conversazione prima di richiamare la competenza e verifica se la richiesta HTTP è stata eseguita correttamente.

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

Si noti che il bot radice include la logica per l'inoltro di attività alla competenza, l'avvio della competenza alla richiesta dell'utente e l'arresto della competenza al completamento.

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

Gestore on turn error

Quando si verifica un errore, l'adapter cancella lo stato della conversazione per reimpostare la conversazione con l'utente ed evitare la persistenza di uno stato di errore.

È consigliabile inviare un'attività di fine conversazione a qualsiasi competenza attiva prima di cancellare lo stato della conversazione nel consumer di competenze. In questo modo, la competenza rilascia tutte le risorse associate alla conversazione tra consumer e competenza prima che il consumer di competenze rilasci la conversazione.

SimpleRootBot\AdapterWithErrorHandler.cs

In questo esempio la logica di errore dei turni viene suddivisa tra alcuni metodi helper.

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

Endpoint delle competenze

Il bot definisce un endpoint che inoltra le attività di competenze in arrivo al gestore di competenze del bot radice.

SimpleRootBot\Controllers\SkillController.cs

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

Registrazione del servizio

Includere un oggetto configurazione dell'autenticazione con qualsiasi convalida delle attestazioni, oltre a tutti gli oggetti aggiuntivi. In questo esempio viene usata la stessa logica di configurazione dell'autenticazione per convalidare le attività di utenti e competenze.

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

Testare il bot radice

È possibile testare il consumer di competenze in Emulator come se fosse un normale bot, tuttavia è necessario eseguire contemporaneamente sia il bot della competenza che il quello del consumer di competenze. Per informazioni su come configurare la competenza, vedere Implementare una competenza.

Scaricare e installare l'ultima versione di Bot Framework Emulator

  1. Eseguire il bot di competenza echo e il bot radice semplice in locale nel computer. Se sono necessarie istruzioni, vedere il file per l'esempio C#, JavaScript, Java o Python.README
  2. Usare Emulator per testare il bot come illustrato di seguito. Quando si invia un end messaggio o stop alla competenza, la competenza invia al bot radice un'attività endOfConversation , oltre al messaggio di risposta. La proprietà code dell'attività endOfConversation indica che la competenza è stata completata.

Trascrizione di esempio di un'interazione con il consumer di competenze.

Altre informazioni sul debug

Poiché il traffico tra competenze e consumer di competenze viene autenticato, è necessario eseguire passaggi aggiuntivi durante il debug di tali bot.

  • Il consumer di competenze e tutte le competenze utilizzate, direttamente o indirettamente, devono essere in esecuzione.
  • Se i bot vengono eseguiti localmente e se uno dei bot ha un ID app e una password, tutti i bot devono avere ID e password validi.
  • Se tutti i bot vengono distribuiti, vedere come eseguire il debug di un bot da qualsiasi canale usando ngrok.
  • Se alcuni bot sono in esecuzione in locale e alcuni vengono distribuiti, vedere come eseguire il debug di una competenza o di un consumer di competenze.

In caso contrario, è possibile eseguire il debug di un consumer di competenze o di una competenza in modo molto simile al debug di altri bot. Per altre informazioni, vedere Debug di un bot e Debug con Bot Framework Emulator.

Informazioni aggiuntive

Ecco alcuni aspetti da considerare quando si implementa un bot radice più complesso.

Per consentire all'utente di annullare una competenza in più fasi

Il bot radice deve controllare il messaggio dell'utente prima di inoltrarlo alla competenza attiva. Se l'utente vuole annullare il processo corrente, il bot radice può inviare un'attività endOfConversation alla competenza, invece di inoltrare il messaggio.

Per eseguire lo scambio di dati tra bot radice e bot di competenza

Per inviare parametri alla competenza, il consumer di competenze può impostare la proprietà value sui messaggi inviati alla competenza. Per ricevere i valori restituiti dalla competenza, il consumer di competenze deve controllare la proprietà value quando la competenza invia un'attività endOfConversation.

Per usare più competenze

  • Se una competenza è attiva, il bot radice deve determinare quale competenza è attiva e inoltrare il messaggio dell'utente alla competenza corretta.
  • Se non ci sono competenze attive, il bot radice deve determinare la competenza da avviare, se disponibile, in base allo stato del bot e all'input dell'utente.
  • Se si vuole consentire all'utente di alternare tra più competenze simultanee, il bot radice deve determinare con quali competenze attive l'utente intende interagire prima di inoltrare il messaggio dell'utente.

Per usare una modalità di recapito delle risposte previste

Per usare la modalità di recapito delle risposte previste:

  • Clonare l'attività dal contesto di turno.
  • Impostare la proprietà modalità di recapito della nuova attività su "ExpectReplies" prima di inviare l'attività dal bot radice alla competenza.
  • Legge le risposte previste dal corpo della risposta invoke restituito dalla risposta della richiesta.
  • Elaborare ogni attività, all'interno del bot radice o inviandola al canale che ha avviato la richiesta originale.

Le risposte previste possono essere utili nelle situazioni in cui il bot che risponde a un'attività deve essere la stessa istanza del bot che ha ricevuto l'attività.