适用于:SDK v4
可以使用技能扩展其他机器人。
技能是可以为另一机器人执行一组任务的机器人,使用清单来描述其接口。
根机器人是面向用户的机器人,可调用一项或多项技能。 根机器人是一种技能使用者。
- 技能使用者可以使用声明验证来管理哪些技能可以访问它。
- 技能使用者可以使用多项技能。
- 无权访问技能源代码的开发人员可以使用技能清单中的信息来设计其技能使用者。
本文演示如何实现一个使用回显技能来回显用户输入的技能使用者。 如需示例技能清单并了解如何实现回显技能,请参阅如何实现技能。
有关如何通过技能对话使用技能的信息,请参阅如何通过对话使用技能。
某些类型的技能使用者无法使用某些类型的技能机器人。
下表介绍支持的组合方式。
|
多租户技能 |
单租户技能 |
用户分配的托管标识技能 |
多租户消费者 |
支持 |
不支持 |
不支持 |
单租户使用者 |
不支持 |
如果两个应用都属于同一租户,则受支持 |
如果两个应用都属于同一租户,则受支持 |
用户分配的托管标识使用者 |
不支持 |
如果两个应用都属于同一租户,则受支持 |
如果两个应用都属于同一租户,则受支持 |
先决条件
注意
从版本 4.11 开始,在 Bot Framework Emulator 中以本地方式测试技能使用者不需要应用 ID 和密码。 要将客户部署到 Azure 或调用已部署的技能,仍然需要 Azure 订阅。
关于此示例
skills-simple-bot-to-bot 示例包含下面的两个机器人的项目:
- 用于实现此技能的回显技能机器人。
- 简单根机器人,其实现的根机器人使用此技能。
本文重点介绍根机器人,其中包括其机器人和适配器对象中的支持逻辑,并包括用于与技能交换活动的对象。 其中包括:
- 用于向技能发送活动的技能客户端。
- 用于从技能接收活动的技能处理程序。
- 技能聊天 ID 工厂,供技能客户端和处理程序用来在用户-根聊天引用和根-技能聊天引用之间进行转换。
有关回显技能机器人的信息,请参阅如何实现技能。
资源
对于部署的机器人,机器人到机器人身份验证要求每个参与的机器人都有有效的标识信息。
但是,无需应用 ID 和密码,即可在本地使用 Emulator 测试多租户技能和技能使用者。
应用程序配置
- (可选)将根机器人的标识信息添加到其配置文件中。 如果技能或技能使用者提供标识信息,则两者都必须提供。
- 添加技能主机终结点(服务或回调 URL),技能应将该主机回复给技能使用者。
- 针对技能使用者将要使用的每项技能添加一个条目。 每个条目包含:
- 一个 ID,供技能使用者用来标识每项技能。
- (可选)技能的应用或客户端 ID。
- 技能的消息传送终结点。
注意
如果技能或技能使用者提供标识信息,则两者都必须提供。
SimpleRootBot\appsettings.js开启
(可选)添加根机器人的标识信息,并为回声技能机器人添加应用程序或客户端 ID。
{
"MicrosoftAppType": "",
"MicrosoftAppId": "",
"MicrosoftAppPassword": "",
"MicrosoftAppTenantId": "",
"SkillHostEndpoint": "http://localhost:3978/api/skills/",
"BotFrameworkSkills": [
{
"Id": "EchoSkillBot",
"AppId": "",
"SkillEndpoint": "http://localhost:39783/api/messages"
}
]
}
echo-skill-bot/.env
(可选)添加根机器人的标识信息,并为回声技能机器人添加应用程序或客户端 ID。
MicrosoftAppType=
MicrosoftAppId=
MicrosoftAppPassword=
MicrosoftAppTenantId=
SkillHostEndpoint=http://localhost:3978/api/skills/
SkillId=EchoSkillBot
SkillAppId=
SkillEndpoint=http://localhost:39783/api/messages
DialogRootBot\application.properties
(可选)添加根机器人的应用 ID 和密码,并将回显技能机器人的应用 ID 添加到 BotFrameworkSkills
数组中。
警告
您正在寻找的示例似乎已经被移动了! 放心,我们正在努力解决此问题。
simple_root_bot/config.py
(可选)添加根机器人的应用程序ID和密码,并为回声技能机器人添加应用程序ID。
APP_ID = os.environ.get("MicrosoftAppId", "")
APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
APP_TYPE = os.environ.get("MicrosoftAppType", "MultiTenant")
APP_TENANTID = os.environ.get("MicrosoftAppTenantId", "")
SKILL_HOST_ENDPOINT = "http://localhost:3978/api/skills"
SKILLS = [
{
"id": "EchoSkillBot",
"app_id": "",
"skill_endpoint": "http://localhost:39783/api/messages",
},
]
# Callers to only those specified, '*' allows any caller.
技能配置
此示例将配置文件中每项技能的信息读取到一组 skill 对象中。
SimpleRootBot\SkillsConfiguration.cs
public class SkillsConfiguration
{
public SkillsConfiguration(IConfiguration configuration)
{
var section = configuration?.GetSection("BotFrameworkSkills");
var skills = section?.Get<BotFrameworkSkill[]>();
if (skills != null)
{
foreach (var skill in skills)
{
Skills.Add(skill.Id, skill);
}
}
var skillHostEndpoint = configuration?.GetValue<string>(nameof(SkillHostEndpoint));
if (!string.IsNullOrWhiteSpace(skillHostEndpoint))
{
SkillHostEndpoint = new Uri(skillHostEndpoint);
}
}
public Uri SkillHostEndpoint { get; }
public Dictionary<string, BotFrameworkSkill> Skills { get; } = new Dictionary<string, BotFrameworkSkill>();
}
simple-root-bot/skillsConfiguration.js
* A helper class that loads Skills information from configuration.
*/
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
警告
您正在寻找的示例似乎已经被移动了! 放心,我们正在努力解决此问题。
simple-root-bot/config.py
SKILLS: Dict[str, BotFrameworkSkill] = {
skill["id"]: BotFrameworkSkill(**skill) for skill in DefaultConfig.SKILLS
}
聊天 ID 工厂
这会创建与技能配合使用的聊天 ID,并可从技能聊天 ID 恢复原始用户聊天 ID。
此示例的聊天 ID 工厂支持一个简单的方案,其中:
- 根机器人旨在使用一种特定的技能。
- 根机器人一次只能与一项技能进行一项活动的聊天。
SDK 提供的 SkillConversationIdFactory
类可跨任何技能使用,无需复制源代码。 对话 ID 中心在 Startup.cs 中配置。
SDK 提供的 SkillConversationIdFactory
类可跨任何技能使用,无需复制源代码。 对话 ID 中心在 index.js 中配置。
Java 已实现 SkillConversationIdFactory 类作为可跨任何技能使用的 SDK 类,无需复制源代码。 可在 botbuilder 包源代码 [Botbuilder JAVA SDK 代码] 中找到 SkillConversationIdFactory 的代码。
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])
若要支持更复杂的方案,请在设计聊天 ID 工厂时确保:
- “创建技能聊天 ID”方法获取或生成相应的技能聊天 ID。
- “获取聊天引用”方法获取正确的用户聊天。
技能客户端和技能处理程序
技能使用者使用技能客户端向技能转发活动。
客户端使用技能配置信息和聊天 ID 工厂来完成此操作。
技能使用者使用技能处理程序从技能接收活动。
处理程序使用对话 ID 工厂、身份验证配置和凭据提供程序来执行这一操作,并依赖于根机器人的适配器和活动处理程序。
SimpleRootBot\Startup.cs
services.AddSingleton<IBotFrameworkHttpAdapter>(sp => sp.GetService<CloudAdapter>());
services.AddSingleton<BotAdapter>(sp => sp.GetService<CloudAdapter>());
simple-root-bot/index.js
// is restarted, anything stored in memory will be gone.
const memoryStorage = new MemoryStorage();
const conversationState = new ConversationState(memoryStorage);
// Create the conversationIdFactory
// Route received a request to adapter for processing
DialogRootBot\application.java
警告
您正在寻找的示例似乎已经被移动了! 放心,我们正在努力解决此问题。
simple-root-bot/app.py
CREDENTIAL_PROVIDER = SimpleCredentialProvider(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
CLIENT = SkillHttpClient(CREDENTIAL_PROVIDER, ID_FACTORY)
ADAPTER, BOT, ID_FACTORY, CREDENTIAL_PROVIDER, AUTH_CONFIG
)
来自技能的 HTTP 流量会进入技能使用者播发到技能的服务 URL 端点。 使用特定于语言的终结点处理程序将流量转发到技能处理程序。
默认技能处理程序如下:
- 如果提供了应用 ID 和密码,则使用身份验证配置对象执行机器人到机器人身份验证和声明验证。
- 使用聊天 ID 工厂从使用者-技能聊天转换回根-用户聊天。
- 生成一条主动消息,使技能使用者可以重新建立根-用户轮次的上下文并向用户转发活动。
活动处理程序逻辑
请注意,技能使用者逻辑应执行以下操作:
- 记住是否有任何活动的技能,并根据需要向它们转发活动。
- 注意用户何时提出某个应转发给技能的请求,并启动技能。
- 从任何活动技能中查找
endOfConversation
活动,这样就可以注意到该活动何时完成。
- 根据需要添加逻辑,让用户或技能使用者取消尚未完成的技能。
- 在对技能进行调用之前保存状态,因为任何响应都可能返回到技能使用者的另一实例。
SimpleRootBot\Bots\RootBot.cs
根机器人依赖于聊天状态、技能信息、技能客户端和常规配置。 ASP.NET 通过依赖项注入提供这些对象。
根机器人还定义了一个聊天状态属性访问器,对哪项技能处于活动状态进行跟踪。
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);
}
此示例有一个帮助程序方法,用于将活动转发给技能。 它在调用技能之前保存聊天状态,并检查 HTTP 请求是否成功。
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}");
}
}
需要注意的是,根机器人包含的逻辑可用于将活动转发到技能、在用户请求时启动技能,以及在技能完成时停止技能。
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
根机器人依赖于聊天状态、技能信息和技能客户端。
根机器人还定义了一个聊天状态属性访问器,对哪项技能处于活动状态进行跟踪。
class RootBot extends ActivityHandler {
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.targetSkill = skillsConfig.skills[targetSkillId];
此示例有一个帮助程序方法,用于将活动转发给技能。 它在调用技能之前保存聊天状态,并检查 HTTP 请求是否成功。
}
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 }`);
需要注意的是,根机器人包含的逻辑可用于将活动转发到技能、在用户请求时启动技能,以及在技能完成时停止技能。
});
// 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.
DialogRootBot\RootBot.java
根机器人依赖于聊天状态、技能信息、技能客户端和常规配置。 ASP.NET 通过依赖项注入提供这些对象。
根机器人还定义了一个聊天状态属性访问器,对哪项技能处于活动状态进行跟踪。
警告
您正在寻找的示例似乎已经被移动了! 放心,我们正在努力解决此问题。
此示例有一个帮助程序方法,用于将活动转发给技能。 它在调用技能之前保存聊天状态,并检查 HTTP 请求是否成功。
警告
您正在寻找的示例似乎已经被移动了! 放心,我们正在努力解决此问题。
需要注意的是,根机器人包含的逻辑可用于将活动转发到技能、在用户请求时启动技能,以及在技能完成时停止技能。
警告
您正在寻找的示例似乎已经被移动了! 放心,我们正在努力解决此问题。
simple-root-bot/bots/root_bot.py
根机器人依赖于聊天状态、技能信息、技能客户端和常规配置。
根机器人还定义了一个聊天状态属性访问器,对哪项技能处于活动状态进行跟踪。
def __init__(
self,
conversation_state: ConversationState,
skills_config: SkillConfiguration,
skill_client: SkillHttpClient,
config: DefaultConfig,
):
self._bot_id = config.APP_ID
self._skill_client = skill_client
self._skills_config = skills_config
self._conversation_state = conversation_state
self._active_skill_property = conversation_state.create_property(
ACTIVE_SKILL_PROPERTY_NAME
)
此示例有一个帮助程序方法,用于将活动转发给技能。 它在调用技能之前保存聊天状态,并检查 HTTP 请求是否成功。
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,
)
需要注意的是,根机器人包含的逻辑可用于将活动转发到技能、在用户请求时启动技能,以及在技能完成时停止技能。
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)
轮次错误处理程序
出现错误时,适配器会清除聊天状态以重置与用户的聊天,避免保持错误状态。
在技能使用者中清除对话状态之前,最好将“聊天结束”活动发送给任何活动技能。 这会使技能在技能使用者释放聊天之前释放与使用者-技能聊天相关联的任何资源。
SimpleRootBot\AdapterWithErrorHandler.cs
在此示例中,轮次错误逻辑在几个帮助程序方法中进行拆分。
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
const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication(process.env, credentialsFactory, authConfig);
// 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'
};
// @ts-ignore
endOfConversation = TurnContext.applyConversationReference(
endOfConversation, TurnContext.getConversationReference(context.activity), true);
await conversationState.saveChanges(context, true);
// @ts-ignore
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);
DialogRootBot\SkillAdapterWithErrorHandler.java
在此示例中,轮次错误逻辑在几个帮助程序方法中进行拆分。
警告
您正在寻找的示例似乎已经被移动了! 放心,我们正在努力解决此问题。
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()
技能终结点
机器人定义一个终结点,该终结点将传入技能活动转发到根机器人的技能处理程序。
SimpleRootBot\Controllers\SkillController.cs
[ApiController]
[Route("api/skills")]
public class SkillController : ChannelServiceController
{
public SkillController(ChannelServiceHandlerBase handler)
: base(handler)
{
}
}
simple-root-bot/index.js
// Route received a request to adapter for processing
await adapter.process(req, res, (context) => bot.run(context));
});
DialogRootBot\Controllers\SkillController.java
警告
您正在寻找的示例似乎已经被移动了! 放心,我们正在努力解决此问题。
simple-root-bot/app.py
APP.router.add_post("/api/messages", messages)
服务注册
包括具有任何声明验证的身份验证配置对象,以及所有其他对象。
此示例使用相同的身份验证配置逻辑来验证用户和技能中的活动。
SimpleRootBot\Startup.cs
// Register the skills configuration class
services.AddSingleton<SkillsConfiguration>();
// Register AuthConfiguration to enable custom claim validation.
services.AddSingleton(sp =>
{
var allowedSkills = sp.GetService<SkillsConfiguration>().Skills.Values.Select(s => s.AppId).ToList();
var claimsValidator = new AllowedSkillsClaimsValidator(allowedSkills);
// If TenantId is specified in config, add the tenant as a valid JWT token issuer for Bot to Skill conversation.
// The token issuer for MSI and single tenant scenarios will be the tenant where the bot is registered.
var validTokenIssuers = new List<string>();
var tenantId = sp.GetService<IConfiguration>().GetSection(MicrosoftAppCredentials.MicrosoftAppTenantIdKey)?.Value;
if (!string.IsNullOrWhiteSpace(tenantId))
{
// For SingleTenant/MSI auth, the JWT tokens will be issued from the bot's home tenant.
// Therefore, these issuers need to be added to the list of valid token issuers for authenticating activity requests.
validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, tenantId));
validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, tenantId));
validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV1, tenantId));
validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV2, tenantId));
}
return new AuthenticationConfiguration
{
ClaimsValidator = claimsValidator,
ValidTokenIssuers = validTokenIssuers
};
});
simple-root-bot/index.js
const { SkillsConfiguration } = require('./skillsConfiguration');
// 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
});
DialogRootBot\Application.java
警告
您正在寻找的示例似乎已经被移动了! 放心,我们正在努力解决此问题。
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)
测试根机器人
可以在 Emulator 中测试技能使用者,就像它是普通机器人一样;但是,你需要同时运行技能和技能使用者机器人。
请参阅如何实现技能,了解如何配置技能。
下载并安装最新的 Bot Framework Emulator
- 在计算机上以本地方式运行回显技能机器人和简单的根机器人。 如需说明,请参阅
README
、JavaScript、Java 或 Python 示例的 文件。
- 使用 Emulator 测试机器人,如下所示。 向技能发送
end
或 stop
消息时,该技能还会向根机器人发送除回复消息以外的 endOfConversation
活动。
endOfConversation
活动的 code 属性指示技能已成功完成。
更多关于调试的信息
由于技能与技能使用者之间的流量要经过身份验证,因此调试此类机器人时会执行额外的步骤。
或者,可以像调试其他机器人一样调试技能使用者或技能。 有关详细信息,请参阅调试机器人和使用 Bot Framework Emulator 执行调试。
下面是实现更复杂的根机器人时要考虑的一些事项。
允许用户取消多步技能
根机器人在将用户的消息转发给活动技能之前,应对该消息进行检查。 如果用户想要取消当前进程,根机器人可以向技能发送 endOfConversation
活动,而不是转发消息。
在根和技能机器人之间交换数据
若要将参数发送给技能,技能使用者可以在其发送给技能的消息上设置 value 属性。 若要从技能接收返回值,技能使用者应在技能发送 活动时检查 endOfConversation
属性。
使用多项技能
- 如果某项技能处于活动状态,则根机器人需要确定哪项技能处于活动状态,然后将用户的消息转发给正确的技能。
- 如果没有技能处于活动状态,则根机器人需要根据机器人状态和用户输入来确定要启动的技能(如果有)。
- 如果希望允许用户在多个并发技能之间切换,则根机器人在转发用户的消息之前需确定用户要与哪项活动技能交互。
使用预期回复的传递模式
若要使用预期答复传递模式:
- 从 turn 上下文克隆活动。
- 在将活动从根机器人发送到技能之前,将新活动的"传递模式"属性设置为 "ExpectReplies"。
- 从请求响应返回的调用响应正文中读取预期的答复。
- 在根机器人中处理每个活动,或将其发送到启动原始请求的通道。
如果需要对活动进行答复的机器人实例与接收活动的机器人实例相同,则预期的回复会非常有用。