Você pode usar habilidades para estender outro bot.
Uma habilidade é um bot que pode executar um conjunto de tarefas para outro bot e usa um manifesto para descrever sua interface.
Um bot raiz é um bot voltado para o usuário que pode invocar uma ou mais habilidades. Um bot raiz é um tipo de consumidor de habilidades.
Um consumidor de habilidades deve usar a validação de reivindicações para gerenciar quais habilidades podem acessá-la.
Um consumidor de competências pode utilizar múltiplas competências.
Os desenvolvedores que não têm acesso ao código-fonte da habilidade podem usar as informações no manifesto da habilidade para projetar seu consumidor de habilidades.
Este artigo demonstra como implementar uma habilidade do consumidor que usa a habilidade de eco para ecoar a entrada do usuário. Para obter um exemplo de manifesto de habilidade e informações sobre como implementar a habilidade de eco, consulte como implementar uma habilidade.
Alguns tipos de consumidores de habilidades não são capazes de usar alguns tipos de bots de habilidades.
A tabela a seguir descreve quais combinações são suportadas.
Habilidade multilocatária
Habilidade de inquilino único
Habilidade de identidade gerenciada atribuída pelo usuário
Consumidor multilocatário
Suportado
Não suportado
Não suportado
Consumidor de inquilino único
Não suportado
Suportado se ambas as aplicações pertencerem ao mesmo inquilino
Suportado se ambas as aplicações pertencerem ao mesmo inquilino
Consumidor de identidade gerenciada atribuído pelo usuário
Não suportado
Suportado se ambas as aplicações pertencerem ao mesmo inquilino
Suportado se ambas as aplicações pertencerem ao mesmo inquilino
Nota
Os SDKs JavaScript, C# e Python do Bot Framework continuarão a ser suportados, no entanto, o Java SDK está sendo desativado com suporte final de longo prazo terminando em novembro de 2023.
Os bots existentes construídos com o Java SDK continuarão a funcionar.
Opcionalmente, uma assinatura do Azure. Se não tiver uma, crie uma conta gratuita antes de começar.
Uma cópia do exemplo de bot-to-bot simples de habilidades em C#, JavaScript, Java ou Python.
Nota
A partir da versão 4.11, você não precisa de um ID de aplicativo e senha para testar um consumidor de habilidades localmente no Bot Framework Emulator. Uma assinatura do Azure ainda é necessária para implantar seu consumidor no Azure ou para consumir uma habilidade implantada.
Sobre este exemplo
O exemplo de habilidades simples de bot-to-bot inclui projetos para dois bots:
O bot de habilidade de eco, que implementa a habilidade.
O bot raiz simples, que implementa um bot raiz que consome a habilidade.
Este artigo se concentra no bot raiz, que inclui lógica de suporte em seus objetos de bot e adaptador e inclui objetos usados para trocar atividades com uma habilidade. Estes são, entre outros:
Um cliente de habilidade, usado para enviar atividades para uma habilidade.
Um manipulador de habilidades, usado para receber atividades de uma habilidade.
Uma fábrica de ID de conversação de habilidade, usada pelo cliente e manipulador de habilidades para traduzir entre a referência de conversação raiz do usuário e a referência de conversa de habilidade raiz.
Para bots implantados, a autenticação de bot para bot requer que cada bot participante tenha informações de identidade válidas.
No entanto, você pode testar habilidades multilocatárias e consumidores de habilidades localmente com o emulador sem um ID de aplicativo e senha.
Configuração da aplicação
Opcionalmente, adicione as informações de identidade do bot raiz ao seu arquivo de configuração. Se o consumidor de habilidade ou habilidade fornece informações de identidade, ambos devem.
Adicione o ponto de extremidade do host de habilidades (o URL de serviço ou retorno de chamada) ao qual as habilidades devem responder ao consumidor de habilidades.
Adicione uma entrada para cada habilidade que o consumidor usará. Cada entrada inclui:
Um ID que o consumidor de habilidades usará para identificar cada habilidade.
Opcionalmente, o aplicativo ou ID do cliente da habilidade.
O ponto de extremidade de mensagens da habilidade.
Nota
Se o consumidor de habilidade ou habilidade fornece informações de identidade, ambos devem.
Opcionalmente, adicione o ID e a senha do aplicativo do bot raiz e adicione o ID do aplicativo para o bot de habilidade de eco à BotFrameworkSkills matriz.
MicrosoftAppId=
MicrosoftAppPassword=
server.port=3978
SkillhostEndpoint=http://localhost:3978/api/skills/
#replicate these three entries, incrementing the index value [0] for each successive Skill that is added.
BotFrameworkSkills[0].Id=EchoSkillBot
BotFrameworkSkills[0].AppId= "Add the App ID for the skill here"
BotFrameworkSkills[0].SkillEndpoint=http://localhost:39783/api/messages
simple_root_bot/config.py
Opcionalmente, adicione o ID e a senha do aplicativo do bot raiz e adicione o ID do aplicativo para o bot de habilidade de eco.
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>();
}
simple-root-bot/skillsConfiguration.js
class SkillsConfiguration {
constructor() {
this.skillsData = {};
// Note: we only have one skill in this sample but we could load more if needed.
const botFrameworkSkill = {
id: process.env.SkillId,
appId: process.env.SkillAppId,
skillEndpoint: process.env.SkillEndpoint
};
this.skillsData[botFrameworkSkill.id] = botFrameworkSkill;
this.skillHostEndpointValue = process.env.SkillHostEndpoint;
if (!this.skillHostEndpointValue) {
throw new Error('[SkillsConfiguration]: Missing configuration parameter. SkillHostEndpoint is required');
}
}
get skills() {
return this.skillsData;
}
get skillHostEndpoint() {
return this.skillHostEndpointValue;
}
}
DialogRootBot\SkillsConfiguration.java
public class SkillsConfiguration {
private URI skillHostEndpoint;
private Map<String, BotFrameworkSkill> skills = new HashMap<String, BotFrameworkSkill>();
public SkillsConfiguration(Configuration configuration) {
boolean noMoreEntries = false;
int indexCount = 0;
while (!noMoreEntries) {
String botID = configuration.getProperty(String.format("BotFrameworkSkills[%d].Id", indexCount));
String botAppId = configuration.getProperty(String.format("BotFrameworkSkills[%d].AppId", indexCount));
String skillEndPoint =
configuration.getProperty(String.format("BotFrameworkSkills[%d].SkillEndpoint", indexCount));
if (
StringUtils.isNotBlank(botID) && StringUtils.isNotBlank(botAppId)
&& StringUtils.isNotBlank(skillEndPoint)
) {
BotFrameworkSkill newSkill = new BotFrameworkSkill();
newSkill.setId(botID);
newSkill.setAppId(botAppId);
try {
newSkill.setSkillEndpoint(new URI(skillEndPoint));
} catch (URISyntaxException e) {
e.printStackTrace();
}
skills.put(botID, newSkill);
indexCount++;
} else {
noMoreEntries = true;
}
}
String skillHost = configuration.getProperty("SkillhostEndpoint");
if (!StringUtils.isEmpty(skillHost)) {
try {
skillHostEndpoint = new URI(skillHost);
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
}
/**
* @return the SkillHostEndpoint value as a Uri.
*/
public URI getSkillHostEndpoint() {
return this.skillHostEndpoint;
}
/**
* @return the Skills value as a Dictionary<String, BotFrameworkSkill>.
*/
public Map<String, BotFrameworkSkill> getSkills() {
return this.skills;
}
}
simples-root-bot/config.py
SKILLS: Dict[str, BotFrameworkSkill] = {
skill["id"]: BotFrameworkSkill(**skill) for skill in DefaultConfig.SKILLS
}
Fábrica de ID de conversação
Isso cria o ID de conversa para uso com a habilidade e pode recuperar o ID de conversação do usuário original do ID de conversação de habilidade.
A fábrica de ID de conversação para este exemplo suporta um cenário simples em que:
O bot raiz é projetado para consumir uma habilidade específica.
O bot raiz tem apenas uma conversa ativa com uma habilidade de cada vez.
O SDK fornece uma SkillConversationIdFactory classe que pode ser usada em qualquer habilidade sem exigir que o código-fonte seja replicado. A fábrica de ID de conversação está configurada em Startup.cs.
O SDK fornece uma SkillConversationIdFactory classe que pode ser usada em qualquer habilidade sem exigir que o código-fonte seja replicado. A fábrica de ID de conversação está configurada em index.js.
Java implementou a classe SkillConversationIdFactory como uma classe SDK que pode ser usada em qualquer habilidade sem exigir que o código-fonte seja replicado. O código para SkillConversationIdFactory pode ser encontrado no código-fonte do pacote botbuilder [botbuilder Java SDK code].
simple-root-bot/skill_conversation_id_factory.py
class SkillConversationIdFactory(ConversationIdFactoryBase):
def __init__(self, storage: Storage):
if not storage:
raise TypeError("storage can't be None")
self._storage = storage
async def create_skill_conversation_id(
self,
options_or_conversation_reference: Union[
SkillConversationIdFactoryOptions, ConversationReference
],
) -> str:
if not options_or_conversation_reference:
raise TypeError("Need options or conversation reference")
if not isinstance(
options_or_conversation_reference, SkillConversationIdFactoryOptions
):
raise TypeError(
"This SkillConversationIdFactory can only handle SkillConversationIdFactoryOptions"
)
options = options_or_conversation_reference
# Create the storage key based on the SkillConversationIdFactoryOptions.
conversation_reference = TurnContext.get_conversation_reference(
options.activity
)
skill_conversation_id = (
f"{conversation_reference.conversation.id}"
f"-{options.bot_framework_skill.id}"
f"-{conversation_reference.channel_id}"
f"-skillconvo"
)
# Create the SkillConversationReference instance.
skill_conversation_reference = SkillConversationReference(
conversation_reference=conversation_reference,
oauth_scope=options.from_bot_oauth_scope,
)
# Store the SkillConversationReference using the skill_conversation_id as a key.
skill_conversation_info = {skill_conversation_id: skill_conversation_reference}
await self._storage.write(skill_conversation_info)
# Return the generated skill_conversation_id (that will be also used as the conversation ID to call the skill).
return skill_conversation_id
async def get_conversation_reference(
self, skill_conversation_id: str
) -> Union[SkillConversationReference, ConversationReference]:
if not skill_conversation_id:
raise TypeError("skill_conversation_id can't be None")
# Get the SkillConversationReference from storage for the given skill_conversation_id.
skill_conversation_info = await self._storage.read([skill_conversation_id])
return skill_conversation_info.get(skill_conversation_id)
async def delete_conversation_reference(self, skill_conversation_id: str):
await self._storage.delete([skill_conversation_id])
Para oferecer suporte a cenários mais complexos, projete sua fábrica de ID de conversação para que:
O método create skill conversation ID obtém ou gera o ID de conversação de habilidade apropriado.
O método de referência get conversation obtém a conversa de usuário correta.
Cliente de habilidades e manipulador de habilidades
O consumidor de habilidades usa um cliente de habilidade para encaminhar atividades para a habilidade.
O cliente usa as informações de configuração de habilidades e a fábrica de ID de conversação para fazer isso.
O consumidor de habilidades usa um manipulador de habilidades para receber atividades de uma habilidade.
O manipulador usa a fábrica de ID de conversação, a configuração de autenticação e um provedor de credenciais para fazer isso, e também tem dependências no adaptador e no manipulador de atividades do bot raiz
O tráfego HTTP da habilidade entrará no ponto de extremidade da URL do serviço que o consumidor da habilidade anuncia para a habilidade. Use um manipulador de ponto de extremidade específico do idioma para encaminhar o tráfego para o manipulador de habilidades.
O manipulador de habilidades padrão:
Se um ID de aplicativo e senha estiverem presentes, use um objeto de configuração de autenticação para executar a autenticação de bot para bot e a validação de declarações.
Usa a fábrica de ID de conversação para traduzir da conversa de habilidade do consumidor de volta para a conversa do usuário raiz.
Gera uma mensagem proativa para que o consumidor de habilidades possa restabelecer um contexto de turno do usuário raiz e encaminhar atividades para o usuário.
Lógica do manipulador de atividades
Note-se que a lógica do consumidor de competências deve:
Lembre-se se existem habilidades ativas e encaminhe atividades para elas, conforme apropriado.
Observe quando um usuário faz uma solicitação que deve ser encaminhada para uma habilidade e inicie a habilidade.
Procure uma endOfConversation atividade de qualquer habilidade ativa, para perceber quando ela é concluída.
Se apropriado, adicione lógica para permitir que o usuário ou consumidor de habilidade cancele uma habilidade que ainda não foi concluída.
Salve o estado antes de fazer a chamada para uma habilidade, pois qualquer resposta pode voltar para uma instância diferente do consumidor de habilidade.
O bot raiz tem dependências no estado da conversação, nas informações de habilidades, no cliente de habilidades e na configuração geral. ASP.NET fornece esses objetos por meio da injeção de dependência.
O bot raiz também define um acessador de propriedade de estado de conversação para rastrear qual habilidade está ativa.
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);
}
Este exemplo tem um método auxiliar para encaminhar atividades para uma habilidade. Ele salva o estado da conversa antes de invocar a habilidade e verifica se a solicitação HTTP foi bem-sucedida.
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}");
}
}
De notar, o bot raiz inclui lógica para encaminhar atividades para a habilidade, iniciar a habilidade a pedido do usuário e parar a habilidade quando a habilidade for concluída.
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);
}
simples-root-bot/rootBot.js
O bot raiz tem dependências no estado da conversação, nas informações de habilidades e no cliente de habilidades.
O bot raiz também define um acessador de propriedade de estado de conversação para rastrear qual habilidade está ativa.
constructor(conversationState, skillsConfig, skillClient, conversationIdFactory) {
super();
if (!conversationState) throw new Error('[RootBot]: Missing parameter. conversationState is required');
if (!skillsConfig) throw new Error('[RootBot]: Missing parameter. skillsConfig is required');
if (!skillClient) throw new Error('[RootBot]: Missing parameter. skillClient is required');
if (!conversationIdFactory) throw new Error('[RootBot]: Missing parameter. conversationIdFactory is required');
this.conversationState = conversationState;
this.skillsConfig = skillsConfig;
this.skillClient = skillClient;
this.conversationIdFactory = conversationIdFactory;
// Create state property to track the active skill
this.activeSkillProperty = this.conversationState.createProperty(RootBot.ActiveSkillPropertyName);
Este exemplo tem um método auxiliar para encaminhar atividades para uma habilidade. Ele salva o estado da conversa antes de invocar a habilidade e verifica se a solicitação HTTP foi bem-sucedida.
async sendToSkill(context, targetSkill) {
// NOTE: Always SaveChanges() before calling a skill so that any activity generated by the skill
// will have access to current accurate state.
await this.conversationState.saveChanges(context, true);
// Create a conversationId to interact with the skill and send the activity
const skillConversationId = await this.conversationIdFactory.createSkillConversationIdWithOptions({
fromBotOAuthScope: context.turnState.get(context.adapter.OAuthScopeKey),
fromBotId: this.botId,
activity: context.activity,
botFrameworkSkill: this.targetSkill
});
// route the activity to the skill
const response = await this.skillClient.postActivity(this.botId, targetSkill.appId, targetSkill.skillEndpoint, this.skillsConfig.skillHostEndpoint, skillConversationId, context.activity);
// Check response status
if (!(response.status >= 200 && response.status <= 299)) {
throw new Error(`[RootBot]: Error invoking the skill id: "${ targetSkill.id }" at "${ targetSkill.skillEndpoint }" (status is ${ response.status }). \r\n ${ response.body }`);
}
}
De notar, o bot raiz inclui lógica para encaminhar atividades para a habilidade, iniciar a habilidade a pedido do usuário e parar a habilidade quando a habilidade for concluída.
// See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types.
this.onMessage(async (context, next) => {
if (context.activity.text.toLowerCase() === 'skill') {
await context.sendActivity('Got it, connecting you to the skill...');
// Set active skill
await this.activeSkillProperty.set(context, this.targetSkill);
// Send the activity to the skill
await this.sendToSkill(context, this.targetSkill);
} else {
await context.sendActivity("Me no nothin'. Say 'skill' and I'll patch you through");
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
// Handle EndOfConversation returned by the skill.
this.onEndOfConversation(async (context, next) => {
// Stop forwarding activities to Skill.
await this.activeSkillProperty.set(context, undefined);
// Show status message, text and value returned by the skill
let eocActivityMessage = `Received ${ ActivityTypes.EndOfConversation }.\n\nCode: ${ context.activity.code }`;
if (context.activity.text) {
eocActivityMessage += `\n\nText: ${ context.activity.text }`;
}
if (context.activity.value) {
eocActivityMessage += `\n\nValue: ${ context.activity.value }`;
}
await context.sendActivity(eocActivityMessage);
// We are back at the root
await context.sendActivity('Back in the root bot. Say \'skill\' and I\'ll patch you through');
// Save conversation state
await this.conversationState.saveChanges(context, true);
// By calling next() you ensure that the next BotHandler is run.
await next();
});
DialogRootBot\RootBot.java
O bot raiz tem dependências no estado da conversação, nas informações de habilidades, no cliente de habilidades e na configuração geral. ASP.NET fornece esses objetos por meio da injeção de dependência.
O bot raiz também define um acessador de propriedade de estado de conversação para rastrear qual habilidade está ativa.
public static final String ActiveSkillPropertyName = "com.microsoft.bot.sample.simplerootbot.ActiveSkillProperty";
private StatePropertyAccessor<BotFrameworkSkill> activeSkillProperty;
private String botId;
private ConversationState conversationState;
private SkillHttpClient skillClient;
private SkillsConfiguration skillsConfig;
private BotFrameworkSkill targetSkill;
public RootBot(
ConversationState conversationState,
SkillsConfiguration skillsConfig,
SkillHttpClient skillClient,
Configuration configuration
) {
if (conversationState == null) {
throw new IllegalArgumentException("conversationState cannot be null.");
}
if (skillsConfig == null) {
throw new IllegalArgumentException("skillsConfig cannot be null.");
}
if (skillClient == null) {
throw new IllegalArgumentException("skillsClient cannot be null.");
}
if (configuration == null) {
throw new IllegalArgumentException("configuration cannot be null.");
}
this.conversationState = conversationState;
this.skillsConfig = skillsConfig;
this.skillClient = skillClient;
botId = configuration.getProperty(MicrosoftAppCredentials.MICROSOFTAPPID);
if (StringUtils.isEmpty(botId)) {
throw new IllegalArgumentException(String.format("%s instanceof not set in configuration",
MicrosoftAppCredentials.MICROSOFTAPPID));
}
// We use a single skill in this example.
String targetSkillId = "EchoSkillBot";
if (!skillsConfig.getSkills().containsKey(targetSkillId)) {
throw new IllegalArgumentException(
String.format("Skill with ID \"%s\" not found in configuration", targetSkillId)
);
} else {
targetSkill = (BotFrameworkSkill) skillsConfig.getSkills().get(targetSkillId);
}
// Create state property to track the active skill
activeSkillProperty = conversationState.createProperty(ActiveSkillPropertyName);
}
Este exemplo tem um método auxiliar para encaminhar atividades para uma habilidade. Ele salva o estado da conversa antes de invocar a habilidade e verifica se a solicitação HTTP foi bem-sucedida.
private CompletableFuture<Void> sendToSkill(TurnContext turnContext, BotFrameworkSkill targetSkill) {
// NOTE: Always SaveChanges() before calling a skill so that any activity generated by the skill
// will have access to current accurate state.
return conversationState.saveChanges(turnContext, true)
.thenAccept(result -> {
// route the activity to the skill
skillClient.postActivity(botId,
targetSkill,
skillsConfig.getSkillHostEndpoint(),
turnContext.getActivity(),
Object.class)
.thenApply(response -> {
// Check response status
if (!(response.getStatus() >= 200 && response.getStatus() <= 299)) {
throw new RuntimeException(
String.format(
"Error invoking the skill id: \"%s\" at \"%s\" (status instanceof %s). \r\n %s",
targetSkill.getId(),
targetSkill.getSkillEndpoint(),
response.getStatus(),
response.getBody()));
}
return CompletableFuture.completedFuture(null);
});
});
}
De notar, o bot raiz inclui lógica para encaminhar atividades para a habilidade, iniciar a habilidade a pedido do usuário e parar a habilidade quando a habilidade for concluída.
@Override
protected CompletableFuture<Void> onMessageActivity(TurnContext turnContext) {
if (turnContext.getActivity().getText().contains("skill")) {
return turnContext.sendActivity(MessageFactory.text("Got it, connecting you to the skill..."))
.thenCompose(result -> {
activeSkillProperty.set(turnContext, targetSkill);
// Send the activity to the skill
return sendToSkill(turnContext, targetSkill);
});
}
// just respond
return turnContext.sendActivity(
MessageFactory.text("Me no nothin'. Say \"skill\" and I'll patch you through"))
.thenCompose(result -> conversationState.saveChanges(turnContext, true));
}
@Override
protected CompletableFuture<Void> onEndOfConversationActivity(TurnContext turnContext) {
// forget skill invocation
return activeSkillProperty.delete(turnContext).thenAccept(result -> {
// Show status message, text and value returned by the skill
String eocActivityMessage = String.format("Received %s.\n\nCode: %s",
ActivityTypes.END_OF_CONVERSATION,
turnContext.getActivity().getCode());
if (!StringUtils.isEmpty(turnContext.getActivity().getText())) {
eocActivityMessage += String.format("\n\nText: %s", turnContext.getActivity().getText());
}
if (turnContext.getActivity() != null && turnContext.getActivity().getValue() != null) {
eocActivityMessage += String.format("\n\nValue: %s", turnContext.getActivity().getValue());
}
turnContext.sendActivity(MessageFactory.text(eocActivityMessage)).thenCompose(sendResult ->{
// We are back at the root
return turnContext.sendActivity(
MessageFactory.text("Back in the root bot. Say \"skill\" and I'll patch you through"))
.thenCompose(secondSendResult-> conversationState.saveChanges(turnContext));
});
});
}
simple-root-bot/bots/root_bot.py
O bot raiz tem dependências no estado da conversação, nas informações de habilidades, no cliente de habilidades e na configuração geral.
O bot raiz também define um acessador de propriedade de estado de conversação para rastrear qual habilidade está ativa.
Este exemplo tem um método auxiliar para encaminhar atividades para uma habilidade. Ele salva o estado da conversa antes de invocar a habilidade e verifica se a solicitação HTTP foi bem-sucedida.
async def __send_to_skill(
self, turn_context: TurnContext, target_skill: BotFrameworkSkill
):
# NOTE: Always SaveChanges() before calling a skill so that any activity generated by the skill
# will have access to current accurate state.
await self._conversation_state.save_changes(turn_context, force=True)
# route the activity to the skill
await self._skill_client.post_activity_to_skill(
self._bot_id,
target_skill,
self._skills_config.SKILL_HOST_ENDPOINT,
turn_context.activity,
)
De notar, o bot raiz inclui lógica para encaminhar atividades para a habilidade, iniciar a habilidade a pedido do usuário e parar a habilidade quando a habilidade for concluída.
async def on_message_activity(self, turn_context: TurnContext):
if "skill" in turn_context.activity.text:
# Begin forwarding Activities to the skill
await turn_context.send_activity(
MessageFactory.text("Got it, connecting you to the skill...")
)
skill = self._skills_config.SKILLS[TARGET_SKILL_ID]
# Save active skill in state
await self._active_skill_property.set(turn_context, skill)
# Send the activity to the skill
await self.__send_to_skill(turn_context, skill)
else:
# just respond
await turn_context.send_activity(
MessageFactory.text(
"Me no nothin'. Say \"skill\" and I'll patch you through"
)
)
async def on_end_of_conversation_activity(self, turn_context: TurnContext):
# forget skill invocation
await self._active_skill_property.delete(turn_context)
eoc_activity_message = f"Received {ActivityTypes.end_of_conversation}.\n\nCode: {turn_context.activity.code}"
if turn_context.activity.text:
eoc_activity_message = (
eoc_activity_message + f"\n\nText: {turn_context.activity.text}"
)
if turn_context.activity.value:
eoc_activity_message = (
eoc_activity_message + f"\n\nValue: {turn_context.activity.value}"
)
await turn_context.send_activity(eoc_activity_message)
# We are back
await turn_context.send_activity(
MessageFactory.text(
'Back in the root bot. Say "skill" and I\'ll patch you through'
)
)
await self._conversation_state.save_changes(turn_context, force=True)
Manipulador de erros por turno
Quando ocorre um erro, o adaptador limpa o estado da conversação para redefinir a conversa com o usuário e evitar a persistência de um estado de erro.
É uma boa prática enviar uma atividade de fim de conversação para qualquer habilidade ativa antes de limpar o estado da conversa no consumidor de habilidades. Isso permite que a habilidade libere todos os recursos associados à conversa de habilidade do consumidor antes que o consumidor de habilidade libere a conversa.
Neste exemplo, a lógica de erro de turno é dividida entre alguns métodos auxiliares.
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}");
}
}
simple-root-bot/index.js
// Create adapter.
// See https://aka.ms/about-bot-adapter to learn more about how bots work.
const adapter = new CloudAdapter(botFrameworkAuthentication);
// Catch-all for errors.
adapter.onTurnError = async (context, error) => {
// This check writes out errors to the console log, instead of to app insights.
// NOTE: In production environment, you should consider logging this to Azure
// application insights. See https://aka.ms/bottelemetry for telemetry
// configuration instructions.
console.error(`\n [onTurnError] unhandled error: ${ error }`);
await sendErrorMessage(context, error);
await endSkillConversation(context);
await clearConversationState(context);
};
async function sendErrorMessage(context, error) {
try {
// Send a message to the user.
let onTurnErrorMessage = 'The bot encountered an error or bug.';
await context.sendActivity(onTurnErrorMessage, onTurnErrorMessage, InputHints.IgnoringInput);
onTurnErrorMessage = 'To continue to run this bot, please fix the bot source code.';
await context.sendActivity(onTurnErrorMessage, onTurnErrorMessage, InputHints.ExpectingInput);
// Send a trace activity, which will be displayed in Bot Framework Emulator.
await context.sendTraceActivity(
'OnTurnError Trace',
`${ error }`,
'https://www.botframework.com/schemas/error',
'TurnError'
);
} catch (err) {
console.error(`\n [onTurnError] Exception caught in sendErrorMessage: ${ err }`);
}
}
async function endSkillConversation(context) {
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.
const activeSkill = await conversationState.createProperty(RootBot.ActiveSkillPropertyName).get(context);
if (activeSkill) {
const botId = process.env.MicrosoftAppId;
let endOfConversation = {
type: ActivityTypes.EndOfConversation,
code: 'RootSkillError'
};
endOfConversation = TurnContext.applyConversationReference(
endOfConversation, TurnContext.getConversationReference(context.activity), true);
await conversationState.saveChanges(context, true);
await skillClient.postActivity(botId, activeSkill.appId, activeSkill.skillEndpoint, skillsConfig.skillHostEndpoint, endOfConversation.conversation.id, endOfConversation);
}
} catch (err) {
console.error(`\n [onTurnError] Exception caught on attempting to send EndOfConversation : ${ err }`);
}
}
async function clearConversationState(context) {
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 page.
await conversationState.delete(context);
} catch (err) {
console.error(`\n [onTurnError] Exception caught on attempting to Delete ConversationState : ${ err }`);
}
}
DialogRootBot\SkillAdapterWithErrorHandler.java
Neste exemplo, a lógica de erro de turno é dividida entre alguns métodos auxiliares.
private class SkillAdapterErrorHandler implements OnTurnErrorHandler {
@Override
public CompletableFuture<Void> invoke(TurnContext turnContext, Throwable exception) {
return sendErrorMessage(turnContext, exception).thenAccept(result -> {
endSkillConversation(turnContext);
}).thenAccept(endResult -> {
clearConversationState(turnContext);
});
}
private CompletableFuture<Void> sendErrorMessage(TurnContext turnContext, Throwable exception) {
try {
// Send a message to the user.
String errorMessageText = "The bot encountered an error or bug.";
Activity errorMessage =
MessageFactory.text(errorMessageText, errorMessageText, InputHints.IGNORING_INPUT);
return turnContext.sendActivity(errorMessage).thenAccept(result -> {
String secondLineMessageText = "To continue to run this bot, please fix the bot source code.";
Activity secondErrorMessage =
MessageFactory.text(secondLineMessageText, secondLineMessageText, InputHints.EXPECTING_INPUT);
turnContext.sendActivity(secondErrorMessage)
.thenApply(
sendResult -> {
// Send a trace activity, which will be displayed in the Bot Framework Emulator.
// Note: we return the entire exception in the value property to help the
// developer;
// this should not be done in production.
return TurnContext.traceActivity(
turnContext,
String.format("OnTurnError Trace %s", exception.toString())
);
}
);
}).thenApply(finalResult -> null);
} catch (Exception ex) {
return Async.completeExceptionally(ex);
}
}
private CompletableFuture<Void> endSkillConversation(TurnContext turnContext) {
if (skillHttpClient == null || skillsConfiguration == null) {
return CompletableFuture.completedFuture(null);
}
// Inform the active skill that the conversation instanceof ended so that it has
// a chance to clean up.
// Note: ActiveSkillPropertyName instanceof set by the RooBot while messages are
// being
StatePropertyAccessor<BotFrameworkSkill> skillAccessor =
conversationState.createProperty(RootBot.ActiveSkillPropertyName);
// forwarded to a Skill.
return skillAccessor.get(turnContext, () -> null).thenApply(activeSkill -> {
if (activeSkill != null) {
String botId = configuration.getProperty(MicrosoftAppCredentials.MICROSOFTAPPID);
Activity endOfConversation = Activity.createEndOfConversationActivity();
endOfConversation.setCode(EndOfConversationCodes.ROOT_SKILL_ERROR);
endOfConversation
.applyConversationReference(turnContext.getActivity().getConversationReference(), true);
return conversationState.saveChanges(turnContext, true).thenCompose(saveResult -> {
return skillHttpClient.postActivity(
botId,
activeSkill,
skillsConfiguration.getSkillHostEndpoint(),
endOfConversation,
Object.class
);
});
}
return CompletableFuture.completedFuture(null);
}).thenApply(result -> null);
}
private CompletableFuture<Void> clearConversationState(TurnContext turnContext) {
try {
return conversationState.delete(turnContext);
} catch (Exception ex) {
return Async.completeExceptionally(ex);
}
}
}
simples-root-bot/adapter_with_error_handler.py
# This check writes out errors to console log
# NOTE: In production environment, you should consider logging this to Azure
# application insights.
print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)
traceback.print_exc()
await self._send_error_message(turn_context, error)
await self._end_skill_conversation(turn_context, error)
await self._clear_conversation_state(turn_context)
async def _send_error_message(self, turn_context: TurnContext, error: Exception):
if not self._skill_client or not self._skill_config:
return
try:
# Send a message to the user.
error_message_text = "The skill encountered an error or bug."
error_message = MessageFactory.text(
error_message_text, error_message_text, InputHints.ignoring_input
)
await turn_context.send_activity(error_message)
error_message_text = (
"To continue to run this bot, please fix the bot source code."
)
error_message = MessageFactory.text(
error_message_text, error_message_text, InputHints.ignoring_input
)
await turn_context.send_activity(error_message)
# Send a trace activity, which will be displayed in Bot Framework Emulator.
await turn_context.send_trace_activity(
label="TurnError",
name="on_turn_error Trace",
value=f"{error}",
value_type="https://www.botframework.com/schemas/error",
)
except Exception as exception:
print(
f"\n Exception caught on _send_error_message : {exception}",
file=sys.stderr,
)
traceback.print_exc()
async def _end_skill_conversation(
self, turn_context: TurnContext, error: Exception
):
if not self._skill_client or not self._skill_config:
return
try:
# Inform the active skill that the conversation is ended so that it has a chance to clean up.
# Note: the root bot manages the ActiveSkillPropertyName, which has a value while the root bot
# has an active conversation with a skill.
active_skill = await self._conversation_state.create_property(
ACTIVE_SKILL_PROPERTY_NAME
).get(turn_context)
if active_skill:
bot_id = self._config.APP_ID
end_of_conversation = Activity(type=ActivityTypes.end_of_conversation)
end_of_conversation.code = "RootSkillError"
TurnContext.apply_conversation_reference(
end_of_conversation,
TurnContext.get_conversation_reference(turn_context.activity),
True,
)
await self._conversation_state.save_changes(turn_context, True)
await self._skill_client.post_activity_to_skill(
bot_id,
active_skill,
self._skill_config.SKILL_HOST_ENDPOINT,
end_of_conversation,
)
except Exception as exception:
print(
f"\n Exception caught on _end_skill_conversation : {exception}",
file=sys.stderr,
)
traceback.print_exc()
async def _clear_conversation_state(self, turn_context: 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" for a Web page.
await self._conversation_state.delete(turn_context)
except Exception as exception:
print(
f"\n Exception caught on _clear_conversation_state : {exception}",
file=sys.stderr,
)
traceback.print_exc()
Ponto final de competências
O bot define um ponto de extremidade que encaminha as atividades de habilidade de entrada para o manipulador de habilidades do bot raiz.
[ApiController]
[Route("api/skills")]
public class SkillController : ChannelServiceController
{
public SkillController(ChannelServiceHandlerBase handler)
: base(handler)
{
}
}
simple-root-bot/index.js
const handler = new CloudSkillHandler(adapter, (context) => bot.run(context), conversationIdFactory, botFrameworkAuthentication);
const skillEndpoint = new ChannelServiceRoutes(handler);
skillEndpoint.register(server, '/api/skills');
DialogRootBot\Controllers\SkillController.java
@RestController
@RequestMapping(value = {"/api/skills"})
public class SkillController extends ChannelServiceController {
public SkillController(ChannelServiceHandler handler) {
super(handler);
}
}
simples-root-bot/app.py
APP.router.add_post("/api/messages", messages)
Registo do serviço
Inclua um objeto de configuração de autenticação com qualquer validação de declarações, além de todos os objetos adicionais.
Este exemplo usa a mesma lógica de configuração de autenticação para validar atividades de usuários e habilidades.
// 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
};
});
simple-root-bot/index.js
// Load skills configuration
const skillsConfig = new SkillsConfiguration();
const allowedSkills = Object.values(skillsConfig.skills).map(skill => skill.appId);
const claimsValidators = allowedCallersClaimsValidator(allowedSkills);
// If the MicrosoftAppTenantId is specified in the environment 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.
let validTokenIssuers = [];
const { MicrosoftAppTenantId } = process.env;
if (MicrosoftAppTenantId) {
// 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 = [
`${ AuthenticationConstants.ValidTokenIssuerUrlTemplateV1 }${ MicrosoftAppTenantId }/`,
`${ AuthenticationConstants.ValidTokenIssuerUrlTemplateV2 }${ MicrosoftAppTenantId }/v2.0/`,
`${ AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV1 }${ MicrosoftAppTenantId }/`,
`${ AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV2 }${ MicrosoftAppTenantId }/v2.0/`
];
}
// Define our authentication configuration.
const authConfig = new AuthenticationConfiguration([], claimsValidators, validTokenIssuers);
const credentialsFactory = new ConfigurationServiceClientCredentialFactory({
MicrosoftAppId: process.env.MicrosoftAppId,
MicrosoftAppPassword: process.env.MicrosoftAppPassword,
MicrosoftAppType: process.env.MicrosoftAppType,
MicrosoftAppTenantId: process.env.MicrosoftAppTenantId
});
const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication(process.env, credentialsFactory, authConfig);
DialogRootBot\Application.java
/**
* This class extends the BotDependencyConfiguration which provides the default
* implementations for a Bot application. The Application class should
* override methods in order to provide custom implementations.
*/
public class Application extends BotDependencyConfiguration {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
/**
* Returns the Bot for this application.
*
* <p>
* The @Component annotation could be used on the Bot class instead of this method
* with the @Bean annotation.
* </p>
*
* @return The Bot implementation for this application.
*/
@Bean
public Bot getBot(
ConversationState conversationState,
SkillsConfiguration skillsConfig,
SkillHttpClient skillClient,
Configuration configuration
) {
return new RootBot(conversationState, skillsConfig, skillClient, configuration);
}
@Override
public AuthenticationConfiguration getAuthenticationConfiguration(Configuration configuration) {
AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration();
authenticationConfiguration.setClaimsValidator(
new AllowedSkillsClaimsValidator(getSkillsConfiguration(configuration)));
return authenticationConfiguration;
}
/**
* Returns a custom Adapter that provides error handling.
*
* @param configuration The Configuration object to use.
* @return An error handling BotFrameworkHttpAdapter.
*/
@Override
public BotFrameworkHttpAdapter getBotFrameworkHttpAdaptor(Configuration configuration) {
return new SkillAdapterWithErrorHandler(
configuration,
getConversationState(new MemoryStorage()),
getSkillHttpClient(
getCredentialProvider(configuration),
getSkillConversationIdFactoryBase(),
getChannelProvider(configuration)),
getSkillsConfiguration(configuration));
}
@Bean
public SkillsConfiguration getSkillsConfiguration(Configuration configuration) {
return new SkillsConfiguration(configuration);
}
@Bean
public SkillHttpClient getSkillHttpClient(
CredentialProvider credentialProvider,
SkillConversationIdFactoryBase conversationIdFactory,
ChannelProvider channelProvider
) {
return new SkillHttpClient(credentialProvider, conversationIdFactory, channelProvider);
}
@Bean
public SkillConversationIdFactoryBase getSkillConversationIdFactoryBase() {
return new SkillConversationIdFactory(getStorage());
}
@Bean public ChannelServiceHandler getChannelServiceHandler(
BotAdapter botAdapter,
Bot bot,
SkillConversationIdFactoryBase conversationIdFactory,
CredentialProvider credentialProvider,
AuthenticationConfiguration authConfig,
ChannelProvider channelProvider
) {
return new SkillHandler(
botAdapter,
bot,
conversationIdFactory,
credentialProvider,
authConfig,
channelProvider);
}
}
simples-root-bot/app.py
# Create adapter.
# See https://aka.ms/about-bot-adapter to learn more about how bots work.
SETTINGS = ConfigurationBotFrameworkAuthentication(
CONFIG,
auth_configuration=AUTH_CONFIG,
)
STORAGE = MemoryStorage()
CONVERSATION_STATE = ConversationState(STORAGE)
ID_FACTORY = SkillConversationIdFactory(STORAGE)
CREDENTIAL_PROVIDER = SimpleCredentialProvider(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
CLIENT = SkillHttpClient(CREDENTIAL_PROVIDER, ID_FACTORY)
ADAPTER = AdapterWithErrorHandler(
SETTINGS, CONFIG, CONVERSATION_STATE, CLIENT, SKILL_CONFIG
)
# Create the Bot
BOT = RootBot(CONVERSATION_STATE, SKILL_CONFIG, CLIENT, CONFIG)
SKILL_HANDLER = SkillHandler(
ADAPTER, BOT, ID_FACTORY, CREDENTIAL_PROVIDER, AUTH_CONFIG
)
# Listen for incoming requests on /api/messages
async def messages(req: Request) -> Response:
# Main bot message handler.
if "application/json" in req.headers["Content-Type"]:
body = await req.json()
else:
return Response(status=HTTPStatus.UNSUPPORTED_MEDIA_TYPE)
activity = Activity().deserialize(body)
auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
invoke_response = await ADAPTER.process_activity(auth_header, activity, BOT.on_turn)
if invoke_response:
return json_response(data=invoke_response.body, status=invoke_response.status)
return Response(status=HTTPStatus.OK)
Testar o bot raiz
Você pode testar a habilidade do consumidor no emulador como se fosse um bot normal; no entanto, você precisa executar os bots de consumidor de habilidade e habilidade ao mesmo tempo.
Veja como implementar uma habilidade para obter informações sobre como configurá-la.
Execute o bot de habilidade de eco e o bot raiz simples localmente em sua máquina. Se precisar de instruções, consulte o README arquivo para o exemplo de C#, JavaScript, Java ou Python.
Use o emulador para testar o bot como mostrado abaixo. Quando você envia uma end mensagem ou stop para a habilidade, a habilidade envia para o bot raiz uma endOfConversation atividade, além da mensagem de resposta. A endOfConversation propriedade code da atividade indica que a habilidade foi concluída com êxito.
Mais sobre depuração
Como o tráfego entre habilidades e consumidores de habilidades é autenticado, há etapas extras ao depurar esses bots.
O consumidor de competências e todas as competências que consome, direta ou indiretamente, devem estar a correr.
Se os bots estiverem sendo executados localmente e se algum deles tiver um ID de aplicativo e senha, todos os bots deverão ter IDs e senhas válidas.
Aqui estão algumas coisas a considerar ao implementar um bot raiz mais complexo.
Para permitir que o usuário cancele uma habilidade de várias etapas
O bot raiz deve verificar a mensagem do usuário antes de encaminhá-la para a habilidade ativa. Se o usuário quiser cancelar o processo atual, o bot raiz pode enviar uma endOfConversation atividade para a habilidade, em vez de encaminhar a mensagem.
Para trocar dados entre os bots raiz e de habilidade
Para enviar parâmetros para a habilidade, o consumidor de habilidade pode definir a propriedade value nas mensagens que envia para a habilidade. Para receber valores de retorno da habilidade, o consumidor de habilidade deve verificar a propriedade value quando a habilidade enviar uma endOfConversation atividade.
Para usar múltiplas habilidades
Se uma habilidade estiver ativa, o bot raiz precisa determinar qual habilidade está ativa e encaminhar a mensagem do usuário para a habilidade correta.
Se nenhuma habilidade estiver ativa, o bot raiz precisará determinar qual habilidade iniciar, se houver, com base no estado do bot e na entrada do usuário.
Se você quiser permitir que o usuário alterne entre várias habilidades simultâneas, o bot raiz precisa determinar com qual das habilidades ativas o usuário pretende interagir antes de encaminhar a mensagem do usuário.
Para usar um modo de entrega de respostas esperadas
Para usar o modo de entrega de respostas esperadas:
Clone a atividade a partir do contexto de turno.
Defina a propriedade do modo de entrega da nova atividade como "ExpectReplies" antes de enviar a atividade do bot raiz para a habilidade.
Leia as respostas esperadas do corpo de resposta de chamada retornadas da resposta da solicitação.
Processe cada atividade, seja dentro do bot raiz ou enviando-a para o canal que iniciou a solicitação original.
As respostas esperadas podem ser úteis em situações em que o bot que responde a uma atividade precisa ser a mesma instância do bot que recebeu a atividade.