Puede usar las aptitudes para ampliar otro bot.
Una aptitud es un bot que puede realizar un conjunto de tareas para otro bot y que usa un manifiesto para describir su interfaz.
Un bot raíz es un bot accesible para el usuario que puede invocar a una o más aptitudes. Un bot raíz es un tipo de consumidor de aptitudes.
Un consumidor de aptitudes debe usar la validación de notificaciones para administrar qué aptitudes pueden tener acceso a él.
Un consumidor de aptitudes puede utilizar varias aptitudes.
Los desarrolladores que no tienen acceso al código fuente de la aptitud pueden usar la información del manifiesto de la aptitud para diseñar su consumidor de aptitudes.
En este artículo se muestra cómo implementar un consumidor de aptitudes que usa la aptitud de eco para repetir los datos de entrada del usuario. Para ver un ejemplo de un manifiesto de aptitud e información sobre cómo implementar la aptitud de eco, consulte cómo implementar una aptitud.
Algunos tipos de consumidores de aptitudes no pueden usar algunos tipos de bots de aptitudes.
Las combinaciones admitidas se describen en la tabla siguiente.
Aptitud multiinquilino
Habilidad de un inquilino único
Aptitud de identidad administrada asignada por el usuario
Consumidor multiinquilino
Compatible
No compatible
No compatible
Consumidor de un solo inquilino
No compatible
Se admite si ambas aplicaciones pertenecen al mismo inquilino
Se admite si ambas aplicaciones pertenecen al mismo inquilino
Consumidor de la identidad administrada asignada por el usuario
No compatible
Se admite si ambas aplicaciones pertenecen al mismo inquilino
Se admite si ambas aplicaciones pertenecen al mismo inquilino
Nota:
Los SDK de JavaScript, C# y Python de Bot Framework seguirán siendo compatibles, pero el SDK de Java se va a retirar con la compatibilidad final a largo plazo que finaliza en noviembre de 2023.
Los bots existentes creados con el SDK de Java seguirán funcionando.
Opcionalmente, una suscripción a Azure. Si no tienes una, crea una cuenta gratuita antes de empezar.
Una copia de la muestra de aptitudes bot-to-bot simple en C#, JavaScript, Java, o Python.
Nota:
A partir de la versión 4.11, no necesita un identificador de aplicación ni una contraseña para probar un consumidor de aptitudes localmente en Bot Framework Emulator. Una suscripción de Azure sigue siendo necesaria para implementar el consumidor en Azure o para consumir una aptitud implementada.
Acerca de este ejemplo
El ejemplo skills simple bot-to-bot incluye proyectos para dos bots:
El bot de aptitud de eco, que implementa la aptitud.
El bot raíz simple, que implementa un bot raíz que utiliza la aptitud.
Este artículo se centra en el bot raíz, que incluye la lógica subyacente en sus objetos de bot y adaptador, e incluye los objetos que se usan para intercambiar actividades con una aptitud. Entre ellas se incluyen las siguientes:
Un cliente de aptitudes, que se usa para enviar actividades a una aptitud.
Un controlador de aptitudes, que se usa para recibir actividades de una aptitud.
Un generador de identificadores de conversación de aptitudes, que el cliente y el controlador de aptitudes utilizan para traducir de la referencia de la conversación entre el bot raíz y el usuario a la referencia de la conversación entre el bot raíz y la aptitud, y viceversa.
Para los bots implementados, la autenticación de bot a bot requiere que cada bot participante tenga información de identidad válida.
Sin embargo, puede probar las aptitudes multiinquilino y los consumidores de aptitudes localmente con Emulator sin un identificador de aplicación y una contraseña.
Configuración de aplicaciones
Opcionalmente, agregue la información de identidad del bot raíz al archivo de configuración. Si el consumidor de aptitudes o aptitudes proporciona información de identidad, ambos deben proporcionarla.
Agregue el punto de conexión del host de aptitudes (el servicio o la URL de devolución de llamada) al que las habilidades deben responder al consumidor de aptitudes.
Agregue una entrada para cada aptitud que usará el consumidor de aptitudes. Cada entrada incluye:
Un identificador que el consumidor de aptitudes usará para identificar cada aptitud.
Opcionalmente, la aplicación o el identificador de cliente de la aptitud.
El punto de conexión de mensajería de la aptitud.
Nota:
Si el consumidor de aptitudes o aptitudes proporciona información de identidad, ambos deben proporcionarla.
Opcionalmente, agregue el identificador y la contraseña de la aplicación del bot raíz y agregue el identificador de aplicación para el bot de aptitud de eco a la matriz BotFrameworkSkills.
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, agregue el identificador y la contraseña de la aplicación del bot raíz y agregue el identificador de aplicación para el bot de aptitud 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;
}
}
simple-root-bot/config.py
SKILLS: Dict[str, BotFrameworkSkill] = {
skill["id"]: BotFrameworkSkill(**skill) for skill in DefaultConfig.SKILLS
}
Generador de identificadores de conversación
Crea el identificador de conversación que se va a usar con la aptitud y puede recuperar el identificador de la conversación original con el usuario a partir del identificador de la conversación con la aptitud.
En este ejemplo, el generador de identificadores de conversación admite un escenario simple en el que:
El bot raíz está diseñado para consumir una aptitud específica.
El bot raíz solo tiene una conversación activa con una aptitud a la vez.
El SDK proporciona una clase SkillConversationIdFactory que se puede usar en cualquier aptitud sin necesidad de replicar el código fuente. El generador de identificadores de conversación está configurado en Startup.cs.
El SDK proporciona una clase SkillConversationIdFactory que se puede usar en cualquier aptitud sin necesidad de replicar el código fuente. El generador de identificadores de conversación está configurado en index.js.
Java ha implementado la clase SkillConversationIdFactory como una clase SDK que se puede usar en cualquier aptitud sin necesidad de replicar el código fuente. El código de SkillConversationIdFactory se puede encontrar en el código fuente del paquete botbuilder [código del SDK de Java botbuilder].
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 admitir escenarios más complejos, diseñe un generador de identificadores de conversación que haga lo siguiente:
El método crear id. de conversación de aptitudes obtiene o genera el identificador de conversación de aptitudes adecuado.
El método obtener referencia de conversación obtiene la conversación de usuario correcta.
Controlador de aptitudes y cliente de aptitudes
El consumidor de aptitudes usa un cliente de aptitudes para reenviar actividades a la aptitud.
El cliente utiliza la información de configuración de las aptitudes y el generador de identificaciones de conversación para hacerlo.
El consumidor de aptitudes usa un controlador de aptitudes para recibir actividades de una aptitud.
El controlador usa el generador de identificaciones de conversación, la configuración de autenticación y un proveedor de credenciales para hacerlo, y también tiene dependencias en el adaptador del bot raíz y en el controlador de actividades.
El tráfico HTTP de la aptitud entrará en la URL del punto de conexión de servicio en el que el consumidor de aptitudes publica información para la aptitud. Use un controlador de puntos de conexión específico del lenguaje para reenviar el tráfico al controlador de aptitudes.
El controlador de aptitudes predeterminado:
Si hay un identificador de aplicación y una contraseña, usa un objeto de configuración de autenticación para realizar la autenticación de bot a bot y la validación de notificaciones.
Usa el generador de identificadores de conversación para traducir de la conversación entre el consumidor y la aptitud de nuevo a la conversación entre el bot raíz y el usuario.
Genera un mensaje activo para que el consumidor de aptitudes pueda volver a establecer el contexto del turno y las actividades de reenvío de la conversación entre el bot raíz y el usuario.
Lógica del controlador de actividades
Tenga en cuenta que la lógica del consumidor de aptitudes debe:
Recordar si hay aptitudes activas y reenviarles las actividades según corresponda.
Identificar si un usuario realiza una solicitud que debe reenviarse a una aptitud, e iniciar la aptitud.
Buscar una actividad endOfConversation de cualquier aptitud activa, para identificar cuándo se completa.
Si es necesario, agregue lógica para que el usuario o el consumidor de aptitudes puedan cancelar una aptitud que aún no ha finalizado.
Guarde el estado antes de realizar la llamada a una aptitud, ya que la respuesta podría volver a una instancia diferente del consumidor de aptitudes
El bot raíz tiene dependencias del estado de la conversación, la información de las aptitudes, el cliente de aptitudes y la configuración general. ASP.NET proporciona estos objetos mediante la inserción de dependencias.
El bot raíz también define un descriptor de acceso a la propiedad de estado de la conversación para realizar el seguimiento de qué aptitud está activa.
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);
}
En este ejemplo hay un método auxiliar para reenviar actividades a una aptitud. Guarda el estado de la conversación antes de invocar la aptitud y comprueba si la solicitud HTTP se realizó correctamente.
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}");
}
}
Tenga en cuenta que el bot raíz incluye lógica para el reenvío de actividades a la aptitud, el inicio de la aptitud en la solicitud del usuario y la detención de la aptitud cuando esta se completa.
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);
}
simple-root-bot/rootBot.js
El bot raíz tiene dependencias del estado de la conversación, la información de las aptitudes y el cliente de aptitudes.
El bot raíz también define un descriptor de acceso a la propiedad de estado de la conversación para realizar el seguimiento de qué aptitud está activa.
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);
En este ejemplo hay un método auxiliar para reenviar actividades a una aptitud. Guarda el estado de la conversación antes de invocar la aptitud y comprueba si la solicitud HTTP se realizó correctamente.
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 }`);
}
}
Tenga en cuenta que el bot raíz incluye lógica para el reenvío de actividades a la aptitud, el inicio de la aptitud en la solicitud del usuario y la detención de la aptitud cuando esta se completa.
// 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
El bot raíz tiene dependencias del estado de la conversación, la información de las aptitudes, el cliente de aptitudes y la configuración general. ASP.NET proporciona estos objetos mediante la inserción de dependencias.
El bot raíz también define un descriptor de acceso a la propiedad de estado de la conversación para realizar el seguimiento de qué aptitud está activa.
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);
}
En este ejemplo hay un método auxiliar para reenviar actividades a una aptitud. Guarda el estado de la conversación antes de invocar la aptitud y comprueba si la solicitud HTTP se realizó correctamente.
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);
});
});
}
Tenga en cuenta que el bot raíz incluye lógica para el reenvío de actividades a la aptitud, el inicio de la aptitud en la solicitud del usuario y la detención de la aptitud cuando esta se completa.
@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
El bot raíz tiene dependencias del estado de la conversación, la información de las aptitudes, el cliente de aptitudes y la configuración general.
El bot raíz también define un descriptor de acceso a la propiedad de estado de la conversación para realizar el seguimiento de qué aptitud está activa.
En este ejemplo hay un método auxiliar para reenviar actividades a una aptitud. Guarda el estado de la conversación antes de invocar la aptitud y comprueba si la solicitud HTTP se realizó correctamente.
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,
)
Tenga en cuenta que el bot raíz incluye lógica para el reenvío de actividades a la aptitud, el inicio de la aptitud en la solicitud del usuario y la detención de la aptitud cuando esta se completa.
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)
Controlador de errores On turn
Cuando se produce un error, el adaptador borra el estado de la conversación para restablecer la conversación con el usuario y evitar que un estado de error persista.
Se recomienda enviar una actividad de final de la conversación las aptitudes activas antes de borrar el estado de la conversación en el consumidor de aptitudes. De este modo, la aptitud libera todos los recursos asociados con la conversación entre el consumidor y la aptitud antes de que el consumidor de aptitudes libere la conversación.
En este ejemplo, la lógica de errores de turno se divide entre algunos 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
En este ejemplo, la lógica de errores de turno se divide entre algunos 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);
}
}
}
simple-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()
Punto de conexión de aptitudes
El bot define un punto de conexión que reenvía las actividades de aptitudes entrantes al controlador de aptitudes del bot raíz.
[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);
}
}
simple-root-bot/app.py
APP.router.add_post("/api/messages", messages)
Registro de servicios
Incluya un objeto de configuración de autenticación con la validación de notificaciones, además de todos los objetos adicionales.
En este ejemplo se usa la misma lógica de configuración de autenticación para validar las actividades de los usuarios y las aptitudes.
// 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);
}
}
simple-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)
Prueba del bot raíz
Puede probar el consumidor de aptitudes en el emulador como si fuera un bot normal; sin embargo, tiene que ejecutar los bots de aptitud y del consumidor de aptitudes al mismo tiempo.
Vea cómo implementar una aptitud para más información sobre cómo configurar la aptitud.
Ejecute el bot de aptitud de eco y el bot raíz simple en su máquina local. Si necesita instrucciones, consulte el archivo README de C#, JavaScript, Java o Python.
Use el emulador para probar el bot tal y como se muestra a continuación. Cuando se envía un mensaje end o stop a la aptitud, esta envía al bot raíz una actividad endOfConversation, además del mensaje de respuesta. La propiedad code de la actividad endOfConversation indica que la aptitud se completó correctamente.
Más sobre la depuración
Dado que se autentica el tráfico entre aptitudes y consumidores de aptitudes, hay pasos adicionales al depurar estos bots.
El consumidor de aptitudes y todas las aptitudes que consume, directa o indirectamente, deben ejecutarse.
Si los bots se ejecutan localmente y si alguno de los bots tiene un identificador de aplicación y una contraseña, todos los bots deben tener identificadores y contraseñas válidos.
Estos son algunos aspectos que se deben tener en cuenta al implementar un bot raíz más complejo.
Para que el usuario pueda cancelar una aptitud de varios pasos
El bot raíz debe comprobar el mensaje del usuario antes de reenviarlo a la aptitud activa. Si el usuario desea cancelar el proceso actual, el bot raíz puede enviar una actividad endOfConversation a la aptitud, en lugar de reenviar el mensaje.
Para intercambiar datos entre el bot raíz y el bot de aptitud
Para enviar parámetros a la aptitud, el consumidor de aptitudes puede establecer la propiedad value en los mensajes que envía a la aptitud. Para recibir los valores devueltos de las aptitudes, el consumidor de aptitudes debe comprobar la propiedad value cuando la aptitud envíe una actividad endOfConversation.
Para usar varias aptitudes
Si hay una aptitud activa, el bot raíz debe determinar cuál y reenviar el mensaje del usuario a la aptitud correcta.
Si no hay ninguna aptitud activa, el bot raíz debe determinar qué aptitud debe iniciarse, según el estado del bot y los datos de entrada del usuario.
Si desea que el usuario pueda cambiar entre varias aptitudes simultáneas, el bot raíz debe determinar con qué aptitudes activas tiene previsto interactuar el usuario antes de reenviar el mensaje del usuario.
Para usar un modo de entrega de respuestas esperadas
Para usar el modo de entrega de respuestas esperadas:
Clone la actividad desde el contexto de turno.
Establezca la propiedad del modo de entrega de la nueva actividad en "ExpectReplies" antes de enviar la actividad desde el bot raíz a la aptitud.
Lea las respuestas esperadas del cuerpo de la respuesta de invocación devuelta de la respuesta de solicitud.
Procese cada actividad, ya sea dentro del bot raíz o envíela al canal que inició la solicitud original.
Las respuestas esperadas pueden ser útiles en situaciones en las que el bot que responde a una actividad debe ser la misma instancia del bot que recibió la actividad.