Sie können Qualifikationen verwenden, um einen anderen Bot zu erweitern.
Eine Qualifikation ist ein Bot, der eine Reihe von Aufgaben für einen anderen Bot durchführen kann und ein Manifest nutzt, um seine Oberfläche zu beschreiben.
Ein Stamm-Bot ist ein Bot für Benutzer, der eine oder mehrere Qualifikationen aufrufen kann. Ein Stamm-Bot ist eine Art von Skill-Consumer.
Ein Skillconsumer muss die Anspruchsvalidierung nutzen, um zu verwalten, welche Skills auf ihn zugreifen können.
Ein Skill-Consumer kann mehrere Qualifikationen verwenden.
Entwickler, die keinen Zugriff auf den Quellcode der Qualifikation haben, können die Informationen im Manifest der Qualifikation nutzen, um ihren „Skill-Consumer“ zu entwerfen.
In diesem Artikel wird veranschaulicht, wie Sie einen Skill-Consumer implementieren, von dem die Echo-Qualifikation (Echo Skill) genutzt wird, um die Eingabe des Benutzers zu wiederholen. Ein Beispiel für ein Qualifikationsmanifest und Informationen zur Implementierung der Echo-Qualifikation finden Sie unter Implementieren einer Qualifikation.
Einige Typen von Skillconsumern sind nicht in der Lage, einige Typen von Skill-Bots zu verwenden.
In der folgenden Tabelle wird beschrieben, welche Kombinationen unterstützt werden.
Mehrinstanzenfähiger Skill
Single-Tenant-Skill
Skill der benutzerseitig zugewiesenen verwalteten Identität
Mehrinstanzenfähige Verbraucher
Unterstützt
Nicht unterstützt
Nicht unterstützt
Einzelinstanzenfähige Verbraucher
Nicht unterstützt
Unterstützt, wenn beide Apps zu demselben Mandanten gehören
Unterstützt, wenn beide Apps zu demselben Mandanten gehören
Consumer einer benutzerseitig zugewiesenen verwalteten Identität
Nicht unterstützt
Unterstützt, wenn beide Apps zu demselben Mandanten gehören
Unterstützt, wenn beide Apps zu demselben Mandanten gehören
Hinweis
Die JavaScript-, C#- und Python-SDKs für Bot Framework werden weiterhin unterstützt, das Java-SDK wird jedoch eingestellt und der langfristige Support endet im November 2023.
Bestehende Bots, die mit dem Java SDK erstellt wurden, werden weiterhin funktionieren.
Ab Version 4.11 benötigen Sie keine App-ID und kein Passwort, um einen Skillconsumer lokal im Bot Framework Emulator zu testen. Ein Azure-Abonnement ist nach wie vor erforderlich, um Ihren Consumer in Azure bereitzustellen oder einen bereitgestellte Skill zu nutzen.
Informationen zu diesem Beispiel
Das Beispiel skills simple bot-to-bot enthält Projekte für zwei Bots:
Echo Skill Bot (Echo-Bot für Qualifikation) zum Implementieren der Qualifikation.
Simple Root Bot (einfacher Stamm-Bot) zum Implementieren eines Stamm-Bots, der die Qualifikation nutzt.
Der Schwerpunkt dieses Artikels liegt auf dem Stamm-Bot, und es wird die Unterstützungslogik in den Bot- und Adapterobjekten beschrieben. Darüber hinaus werden Objekte beschrieben, die zum Austauschen von Aktivitäten mit einer Qualifikation verwendet werden. Dazu gehören:
Ein Skill-Client, der zum Senden von Aktivitäten an eine Qualifikation verwendet wird.
Ein Skill-Handler, der zum Empfangen von Aktivitäten von einer Qualifikation verwendet wird.
Eine Konversations-ID-Factory für Qualifikationen, die vom Skill-Client und -Handler genutzt wird, um die Übersetzung zwischen dem Benutzer/Stamm-Konversationsverweis und dem Stamm/Qualifikation-Konversationsverweis durchzuführen.
Bei eingesetzten Bots erfordert die Bot-zu-Bot-Authentifizierung, dass jeder teilnehmende Bot über gültige Identitätsinformationen verfügt.
Sie können jedoch Mandanten-Skills und Skillconsumer mit dem Emulator lokal ohne App-ID und Passwort testen.
Anwendungskonfiguration
Fügen Sie optional die Identitätsinformationen des Stamm-Bots zur Konfigurationsdatei hinzu. Wenn entweder der Skill oder der Skillconsumer Identitätsinformationen liefert, müssen beide dies tun.
Fügen Sie den Skill-Host-Endpunkt (die Dienst- oder Rückruf-URL) hinzu, an die die Antwort der Skills an den Skillconsumer erfolgen soll.
Fügen Sie einen Eintrag für jede Qualifikation hinzu, die vom Skill-Consumer verwendet wird. Jeder Eintrag umfasst Folgendes:
Eine ID, die vom Skill-Consumer verwendet wird, um die einzelnen Qualifikationen zu identifizieren.
Optional kann die App oder Client-ID des Skill verwendet werden.
Den Messagingendpunkt der Qualifikation.
Hinweis
Wenn entweder der Skill oder der Skillconsumer Identitätsinformationen liefert, müssen beide dies tun.
Fügen Sie optional die App-ID und das Passwort des Stamm-Bots und die App-ID für den Echo-Skill-Bot zum BotFrameworkSkills-Array hinzu.
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
Fügen Sie optional die App-ID und das Passwort des Stamm-Bots und die App-ID für den Echo-Skill-Bot hinzu.
In diesem Beispiel werden Informationen zu den einzelnen Qualifikationen in die Konfigurationsdatei einer Sammlung mit Objekten vom Typ Qualifikation eingelesen.
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
}
Konversations-ID-Factory
Hiermit wird die Konversations-ID für die Qualifikation erstellt, und die ursprüngliche ID der Benutzerkonversation kann aus der Konversations-ID der Qualifikation wiederhergestellt werden.
Die Konversations-ID-Factory für dieses Beispiel unterstützt ein einfaches Szenario, für das Folgendes gilt:
Der Stamm-Bot ist dafür ausgelegt, eine bestimmte Qualifikation zu nutzen.
Der Stamm-Bot verfügt jeweils nur über eine aktive Konversation mit einer Qualifikation.
Das SDK stellt eine SkillConversationIdFactory-Klasse bereit, die über alle Skills hinweg verwendet werden kann, ohne dass der Quellcode repliziert werden muss. Die Unterhaltungs-ID-Zuordnungsinstanz ist in Startup.cs konfiguriert.
Das SDK stellt eine SkillConversationIdFactory-Klasse bereit, die über alle Skills hinweg verwendet werden kann, ohne dass der Quellcode repliziert werden muss. Die Unterhaltungs-ID-Zuordnungsinstanz ist in index.js konfiguriert.
Java hat die Klasse SkillConversationIdFactory als SDK-Klasse implementiert, die für jeden Skill verwendet werden kann, ohne dass der Quellcode repliziert werden muss. Der Code für SkillConversationIdFactory befindet sich im Botbuilder-Paketquellcode [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])
Entwerfen Sie Ihre Konversations-ID-Factory wie folgt, um komplexere Szenarien zu unterstützen:
Mit der create skill conversation ID-Methode wird die entsprechende Konversations-ID für eine Qualifikation abgerufen bzw. generiert.
Mit der get conversation reference-Methode wird die richtige Benutzerkonversation abgerufen.
Skill-Client und Skill-Handler
Der Skill-Consumer verwendet einen Skill-Client, um Aktivitäten an die Qualifikation weiterzuleiten.
Der Client nutzt hierfür die Konfigurationsinformationen für die Qualifikation und die Konversations-ID-Factory.
Der Skill-Consumer nutzt einen Skill-Handler, um Aktivitäten von einer Qualifikation zu empfangen.
Der Handler nutzt hierfür die Konversations-ID-Factory, die Authentifizierungskonfiguration und einen Anmeldeinformationsanbieter und verfügt zudem über Abhängigkeiten vom Adapter und Aktivitätshandler des Stamm-Bots.
Der HTTP-Datenverkehr der Qualifikation fließt an den URL-Dienstendpunkt, der vom Skill-Consumer für die Qualifikation angekündigt wird. Verwenden Sie einen sprachspezifischen Endpunkthandler, um Datenverkehr an den Skill-Handler weiterzuleiten.
Für den Skill-Standardhandler gilt Folgendes:
Wenn eine Anwendungs-ID und ein Passwort vorhanden sind, wird ein Authentifizierungs-Konfigurationsobjekt verwendet, um die Authentifizierung zwischen Bots und die Überprüfung von Ansprüchen durchzuführen.
Verwendet die Konversations-ID-Factory, um die Rückübersetzung von der Consumer/Qualifikation-Konversation in die Stamm/Benutzer-Konversation durchzuführen.
Generiert eine proaktive Nachricht, damit der Skill-Consumer wieder einen Stamm/Benutzer-Durchlaufkontext herstellen und Aktivitäten an den Benutzer weiterleiten kann.
Aktivitätshandlerlogik
In Bezug auf die Logik des Skill-Consumers sollte Folgendes beachtet werden:
Ermitteln Sie, ob aktive Qualifikationen vorhanden sind, und leiten Sie dafür je nach Bedarf Aktivitäten weiter.
Achten Sie darauf, wann ein Benutzer eine Anforderung sendet, die an eine Qualifikation weitergeleitet werden soll, und starten Sie dann die Qualifikation.
Suchen Sie nach einer endOfConversation-Aktivität einer beliebigen aktiven Qualifikation, um verfolgen zu können, wann sie abgeschlossen wird.
Fügen Sie Logik hinzu (falls zutreffend), mit der der Benutzer oder Skill-Consumer eine Qualifikation abbrechen kann, die noch nicht abgeschlossen wurde.
Speichern Sie den Zustand, bevor Sie eine Qualifikation aufrufen, da es sein kann, dass eine Antwort an eine andere Instanz des Skill-Consumers zurückgesendet wird.
Der Stamm-Bot verfügt über Abhängigkeiten in Bezug auf den Konversationszustand, die Qualifikationsinformationen, den Skill-Client und die allgemeine Konfiguration. ASP.NET stellt diese Objekte per Abhängigkeitsinjektion bereit.
Der Stamm-Bot definiert auch einen Eigenschaftenaccessor für den Konversationszustand, um nachzuverfolgen, welche Qualifikation aktiv ist.
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);
}
Dieses Beispiel enthält eine Hilfsmethode für die Weiterleitung von Aktivitäten an eine Qualifikation. Der Konversationszustand wird gespeichert, bevor die Qualifikation aufgerufen wird, und es wird überprüft, ob die HTTP-Anforderung erfolgreich war.
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}");
}
}
Beachten Sie, dass der Stammbot Logik für das Weiterleiten von Aktivitäten an den Skill beinhaltet, mit der der Skill auf Anforderung des Benutzers gestartet und bei Abschluss des Skills beendet wird.
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
Der Stamm-Bot verfügt über Abhängigkeiten in Bezug auf den Konversationszustand, die Qualifikationsinformationen und den Skill-Client.
Der Stamm-Bot definiert auch einen Eigenschaftenaccessor für den Konversationszustand, um nachzuverfolgen, welche Qualifikation aktiv ist.
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);
Dieses Beispiel enthält eine Hilfsmethode für die Weiterleitung von Aktivitäten an eine Qualifikation. Der Konversationszustand wird gespeichert, bevor die Qualifikation aufgerufen wird, und es wird überprüft, ob die HTTP-Anforderung erfolgreich war.
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 }`);
}
}
Beachten Sie, dass der Stammbot Logik für das Weiterleiten von Aktivitäten an den Skill beinhaltet, mit der der Skill auf Anforderung des Benutzers gestartet und bei Abschluss des Skills beendet wird.
// 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
Der Stamm-Bot verfügt über Abhängigkeiten in Bezug auf den Konversationszustand, die Qualifikationsinformationen, den Skill-Client und die allgemeine Konfiguration. ASP.NET stellt diese Objekte per Abhängigkeitsinjektion bereit.
Der Stamm-Bot definiert auch einen Eigenschaftenaccessor für den Konversationszustand, um nachzuverfolgen, welche Qualifikation aktiv ist.
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);
}
Dieses Beispiel enthält eine Hilfsmethode für die Weiterleitung von Aktivitäten an eine Qualifikation. Der Konversationszustand wird gespeichert, bevor die Qualifikation aufgerufen wird, und es wird überprüft, ob die HTTP-Anforderung erfolgreich war.
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);
});
});
}
Beachten Sie, dass der Stammbot Logik für das Weiterleiten von Aktivitäten an den Skill beinhaltet, mit der der Skill auf Anforderung des Benutzers gestartet und bei Abschluss des Skills beendet wird.
@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
Der Stamm-Bot verfügt über Abhängigkeiten in Bezug auf den Konversationszustand, die Qualifikationsinformationen, den Skill-Client und die allgemeine Konfiguration.
Der Stamm-Bot definiert auch einen Eigenschaftenaccessor für den Konversationszustand, um nachzuverfolgen, welche Qualifikation aktiv ist.
Dieses Beispiel enthält eine Hilfsmethode für die Weiterleitung von Aktivitäten an eine Qualifikation. Der Konversationszustand wird gespeichert, bevor die Qualifikation aufgerufen wird, und es wird überprüft, ob die HTTP-Anforderung erfolgreich war.
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,
)
Beachten Sie, dass der Stammbot Logik für das Weiterleiten von Aktivitäten an den Skill beinhaltet, mit der der Skill auf Anforderung des Benutzers gestartet und bei Abschluss des Skills beendet wird.
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)
OnTurn-Fehlerhandler
Wenn ein Fehler auftritt, löscht der Adapter den Konversationszustand, um die Konversation mit dem Benutzer zurückzusetzen und einen dauerhaften Fehlerzustand zu vermeiden.
Es ist eine bewährte Vorgehensweise, eine Aktivität vom Typ end of conversation an aktive Qualifikationen zu senden, bevor der Konversationszustand im Skill-Consumer gelöscht wird. Auf diese Weise kann die Qualifikation alle Ressourcen freigeben, die der Consumer/Qualifikation-Konversation zugeordnet sind, bevor der Skill-Consumer die Konversation freigibt.
In diesem Beispiel ist die Durchlauffehlerlogik auf mehrere Hilfsmethoden aufgeteilt.
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
In diesem Beispiel ist die Durchlauffehlerlogik auf mehrere Hilfsmethoden aufgeteilt.
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()
Endpunkt für Qualifikation
Der Bot definiert einen Endpunkt, mit dem eingehende Qualifikationsaktivitäten an den Skill-Handler des Stamm-Bots weitergeleitet werden.
[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)
Dienstregistrierung
Fügen Sie ein Objekt für die Authentifizierungskonfiguration mit den zugehörigen Anspruchsüberprüfungen sowie alle zusätzlichen Objekte hinzu.
In diesem Beispiel wird die gleiche Authentifizierungs-Konfigurationslogik zum Überprüfen von Aktivitäten von Benutzern und Skills verwendet.
// 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)
Testen des Stamm-Bots
Sie können den Skill-Consumer im Emulator wie einen regulären Bot testen. Sie müssen aber sowohl den Bot für die Qualifikation als auch den Bot für den Skill-Consumer ausführen.
Informationen zum Konfigurieren der Qualifikation finden Sie unter Implementieren einer Qualifikation.
Laden Sie die aktuelle Version von Bot Framework Emulator herunter, und installieren Sie sie.
Führen Sie den „Echo Skill Bot“ und den „Simple Root Bot“ lokal auf Ihrem Computer aus. Wenn Sie eine Anleitung benötigen, helfen Ihnen die README-Datei des Beispiels für C#, JavaScript, Java oder Python weiter.
Verwenden Sie den Emulator, um den Bot wie unten dargestellt zu testen. Wenn Sie eine end- oder stop-Nachricht an die Qualifikation senden, sendet die Qualifikation zusätzlich zur Antwortnachricht eine endOfConversation-Aktivität an den Stamm-Bot. Die code-Eigenschaft der endOfConversation-Aktivität gibt an, dass die Qualifikation erfolgreich abgeschlossen wurde.
Mehr über Debuggen
Da der Datenverkehr zwischen Skills und Skill-Verbraucher authentifiziert wird, gibt es zusätzliche Schritte beim Debuggen solcher Bots.
Der Skill-Verbraucher und alle Skills, die er direkt oder indirekt verbraucht, müssen ausgeführt werden.
Wenn die Bots lokal ausgeführt werden und einer der Bots über eine App-ID und ein Passwort verfügt, müssen alle Bots gültige IDs und Passwörter besitzen.
Hier werden einige Aspekte beschrieben, die Sie beim Implementieren eines komplexeren Stamm-Bots berücksichtigen sollten.
So lassen Sie für Benutzer das Abbrechen einer Qualifikation mit mehreren Schritten zu
Der Stamm-Bot sollte die Nachricht des Benutzers überprüfen, bevor er sie an die aktive Qualifikation weiterleitet. Wenn der Benutzer den aktuellen Prozess abbrechen möchte, kann der Stamm-Bot eine endOfConversation-Aktivität an die Qualifikation senden, anstatt die Nachricht weiterzuleiten.
So tauschen Sie die Daten zwischen Stamm-Bots und Skill-Bots aus
Zum Senden von Parametern an die Qualifikation kann der Skill-Consumer die value-Eigenschaft für die Nachrichten festlegen, die er an die Qualifikation sendet. Der Skill-Consumer sollte die value-Eigenschaft überprüfen, wenn die Qualifikation eine endOfConversation-Aktivität sendet, um Rückgabewerte von der Qualifikation zu erhalten.
So verwenden Sie mehrere Qualifikationen
Wenn eine Qualifikation aktiv ist, muss der Stamm-Bot ermitteln, welche Qualifikation dies ist, und die Nachricht des Benutzers an die richtige Qualifikation weiterleiten.
Falls keine Qualifikation aktiv ist, muss der Stamm-Bot anhand des Botzustands und der Eingabe des Benutzers ermitteln, welche Qualifikation gestartet werden soll (falls zutreffend).
Wenn Sie für Benutzer den Wechsel zwischen mehreren gleichzeitigen Qualifikationen zulassen möchten, muss der Stamm-Bot ermitteln, mit welcher aktiven Qualifikation der Benutzer interagieren möchte, bevor die Nachricht des Benutzers weitergeleitet wird.
So verwenden Sie einen Zustellungsmodus für erwartete Antworten
So verwenden Sie einen Zustellungsmodus für erwartete Antworten:
Klonen Sie die Aktivität aus dem Turnkontext.
Legen Sie die Eigenschaft des Zustellungsmodus der neuen Aktivität auf „ExpectReplies“ fest, bevor Sie die Aktivität vom Stamm-Bot an Skills senden.
Lesen sie erwartete Antworten aus dem Aufrufantwort-Text, der von der Anforderungsantwort zurückgegeben wird.
Verarbeiten Sie jede Aktivität, entweder innerhalb des Stamm-Bots oder durch Senden an den Kanal, der die ursprüngliche Anforderung initiiert hat.
Antworten zu erwarten kann in Situationen nützlich sein, in denen der Bot, der auf eine Aktivität antwortet, dieselbe Instance des Bots sein muss, der die Aktivität erhalten hat.