ОБЛАСТЬ ПРИМЕНЕНИЯ: ПАКЕТ SDK версии 4
Вы можете использовать навыки для расширения функциональности другого бота.
Умение – это бот, который может выполнять набор задач для другого бота и использует манифест для описания своего интерфейса.
Корневым ботом называется бот, который взаимодействует с пользователем и может вызывать один или несколько навыков. Корневой бот представляет собой вид потребителя навыка.
- Пользователь навыков должен использовать верификацию, чтобы управлять правами доступа навыков к ней.
- Потребитель навыка может использовать несколько навыков.
- Разработчики, у которых нет доступа к исходному коду навыка, могут использовать информацию из манифеста этого навыка для проектирования пользователя навыка.
В этой статье демонстрируется создание потребителя навыка, который использует эхо-навык, чтобы воспроизводить ввод пользователя. Пример манифеста навыка и сведения о реализации эхо-навыка см. в статье о реализации навыка.
Сведения об использовании диалога для использования навыка см. в статье Использование диалога для выполнения навыка.
Некоторые пользователи навыков не могут использовать определенные навыковые боты.
В следующей таблице описывается, какие сочетания поддерживаются.
| |
Навыки с несколькими клиентами |
Однотенантный навык |
Навык управляемой идентичности, назначаемой пользователем |
|
Потребитель с несколькими клиентами |
Поддерживается |
Не поддерживается |
Не поддерживается |
|
Потребитель с одним клиентом |
Не поддерживается |
Поддерживается, если оба приложения принадлежат к одному клиенту |
Поддерживается, если оба приложения принадлежат к одному клиенту |
|
Потребитель пользовательской управляемой идентификации |
Не поддерживается |
Поддерживается, если оба приложения принадлежат к одному клиенту |
Поддерживается, если оба приложения принадлежат к одному клиенту |
Это важно
Пакет SDK Bot Framework и эмулятор Bot Framework были архивированы на GitHub. Проект больше не обновляется или не поддерживается. Запросы на поддержку пакета SDK Bot Framework больше не будут обслуживаться с 31 декабря 2025 г.
Чтобы создавать агенты с помощью выбранной службы ИИ, оркестрации и знаний, рекомендуется использовать пакет SDK для агентов Microsoft 365. Пакет SDK для агентов поддерживает язык C#, JavaScript или Python. Дополнительные сведения о пакете SDK для агентов см. в aka.ms/agents.
Если у вас есть существующий бот, созданный с помощью пакета SDK Bot Framework, вы можете обновить бота до пакета SDK для агентов. Ознакомьтесь с основными изменениями и обновлениями в руководстве по миграции с Bot Framework SDK на Agents SDK.
Если вы создаете агент для совместной работы в Microsoft Teams, рассмотрите возможность использования пакета SDK Teams. Он предоставляет интерфейсы API, поддержку адаптивных карт и встроенную оркестрацию ИИ для агентов, работающих в среде Teams. Дополнительные сведения см. в пакете SDK Teams (библиотека ИИ Teams).
Если вы ищете платформу агента на основе SaaS, рассмотрите microsoft Copilot Studio.
Предварительные требования
Примечание.
Начиная с версии 4.11, вам не нужен идентификатор приложения и пароль для локального тестирования потребителя навыка в эмуляторе Bot Framework. Подписка Azure по-прежнему необходима для развертывания клиента в Azure или использования ранее развернутого навыка.
Об этом примере
В пример простых возможностей взаимодействия между ботами включены проекты для двух ботов:
-
бот эхо-навыка, который реализует этот навык;
-
простой корневой бот, который реализует корневого бота, использующего этот навык.
Эта статья посвящена корневому боту, в том числе логике поддержки в объектах бота и адаптера, а также объектам, которые используются для обмена действиями с помощью навыков. Например:
- Клиент навыка, который используется для отправки действий в навык.
- Обработчик умений, который используется для получения активностей от умения.
- Фабрика идентификаторов бесед с навыками, которую клиент и обработчик навыков используют для взаимного преобразования ссылок между беседами пользователя с корневым ботом и корневого бота с навыком.
Сведения о боте с эхо-навыком см. в статье о реализации навыка.
Ресурсы
Для развернутых ботов проверка подлинности бот-к-боту требует, чтобы каждый участвующий бот имел действительные удостоверения личности.
Однако вы можете локально протестировать многотенантные навыки и использование навыков с помощью эмулятора без пароля и ID приложения.
Конфигурация приложений
- При необходимости добавьте данные удостоверения корневого бота в файл конфигурации. Если либо навык, либо пользователь навыка предоставляет информацию о личности, должны оба.
- Добавьте конечную точку узла навыка (URL-адрес службы или обратного вызова), по которой скиллы должны отправлять ответы потребителю навыка.
- Добавьте запись для каждого навыка, который будет использоваться потребителем навыка. Каждая запись включает:
- идентификатор, который потребитель навыка использует для идентификации навыка;
- Опционально, приложение или идентификатор клиента навыка.
- конечная точка навыка для обмена сообщениями.
Примечание.
Если либо навык, либо пользователь навыка предоставляет информацию о личности, должны оба.
SimpleRootBot\appsettings.jsвключен
При необходимости добавьте информацию о личности корневого бота и добавьте ID приложения или клиента для бота навыка Echo.
{
"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 приложения или клиента для бота навыка Echo.
MicrosoftAppType=
MicrosoftAppId=
MicrosoftAppPassword=
MicrosoftAppTenantId=
SkillHostEndpoint=http://localhost:3978/api/skills/
SkillId=EchoSkillBot
SkillAppId=
SkillEndpoint=http://localhost:39783/api/messages
DialogRootBot\application.properties
При необходимости добавьте идентификатор приложения и пароль для корневого бота, а также добавьте идентификатор приложения для бота с навыком эхо в массив BotFrameworkSkills.
MicrosoftAppId=
MicrosoftAppPassword=
server.port=3978
SkillhostEndpoint=http://localhost:3978/api/skills/
#replicate these three entries, incrementing the index value [0] for each successive Skill that is added.
BotFrameworkSkills[0].Id=EchoSkillBot
BotFrameworkSkills[0].AppId= "Add the App ID for the skill here"
BotFrameworkSkills[0].SkillEndpoint=http://localhost:39783/api/messages
simple_root_bot/config.py
При необходимости добавьте идентификатор и пароль приложения корневого бота, а также добавьте идентификатор приложения для бота с эхо-навыком.
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
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
}
Фабрика идентификаторов разговора
Этот элемент создает идентификатор беседы для использования с навыком и может восстановить исходный идентификатор беседы пользователя из идентификатора беседы с навыком.
Фабрика идентификаторов разговоров для этого примера поддерживает простой сценарий, где:
- Корневой бот предназначен для потребления одного конкретного навыка.
- корневой бот поддерживает только одну активную беседу с навыком в конкретный момент времени.
Пакет SDK предоставляет SkillConversationIdFactory класс, который можно использовать для любого навыка, не требуя репликации исходного кода. Фабрика идентификаторов беседы настроена в Startup.cs.
Пакет SDK предоставляет SkillConversationIdFactory класс, который можно использовать для любого навыка, не требуя репликации исходного кода. Фабрика идентификаторов разговоров настроена в index.js.
Java реализовал класс SkillConversationIdFactory в качестве класса SDK, который можно использовать для любого навыка, не требуя репликации исходного кода. Код для SkillConversationIdFactory можно найти в исходном коде пакета botbuilder [код пакета botbuilder Java SDK].
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])
Чтобы поддерживать более сложные сценарии, разработайте фабрику идентификаторов бесед таким образом, чтобы:
- Метод создания идентификатора для беседы с навыком получает или генерирует подходящий идентификатор для этой беседы.
- метод получения ссылки на беседу получает доступ к правильной беседе с пользователем.
Клиент навыка и обработчик навыка
Потребитель навыка использует приложение навыка, чтобы передавать задачи в навык.
Клиент использует сведения о конфигурации навыков и фабрику идентификаторов бесед для этого.
Потребитель навыка использует обработчик навыка для получения действий от навыка.
Обработчик использует для этого фабрику идентификаторов разговоров, конфигурацию проверки подлинности и поставщика учетных данных, а также имеет зависимости от адаптера корневого бота и обработчика активности.
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
@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
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-адреса службы, которую пользователь навыка предоставляет навыку. Для передачи трафика обработчику навыка примените обработчик конечной точки для выбранного языка.
Обработчик навыка по умолчанию:
- Если идентификатор приложения и пароль присутствуют, объект конфигурации проверки подлинности используется для выполнения как аутентификации ботов между собой, так и проверки валидности утверждений.
- использует фабрику идентификаторов бесед для преобразования беседы потребителя навыков обратно в беседу с конечным пользовательским интерфейсом.
- создает упреждающее сообщение, чтобы пользователь навыка мог восстановить контекст взаимодействия с корневым пользователем и пересылать действия пользователю.
Логика обработчика действий
Обратите внимание, что логика потребителя навыка должна обеспечить следующее:
- Запомните, есть ли какие-либо активные навыки, и направляйте действия в них так, как это уместно.
- Обнаружьте, когда пользователь делает запрос, который нужно передать в навык, и запустите этот навык.
- Ищите действие
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 эти объекты реализуются путем внедрения зависимостей.
Также корневой бот определяет метод доступа к свойству состояния беседы, чтобы отслеживать активные навыки.
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);
}
В нашем примере есть вспомогательный метод для перенаправления действий в навык. Он сохраняет состояние беседы перед вызовом навыка и проверяет, успешно ли выполнен HTTP-запрос.
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);
});
});
}
Обратите внимание, что в корневом элементе бота предусмотрена логика для перенаправления действий в навык, запуска навыка по запросу пользователя и остановки навыка по завершении выполнения навыка.
@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
Корневой бот имеет зависимости от состояния диалога, сведений о навыках, клиента навыка и общей конфигурации.
Также корневой бот определяет метод доступа к свойству состояния беседы, чтобы отслеживать активные навыки.
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
В нашем примере логика ошибок очередности распределена между несколькими вспомогательными методами.
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()
Конечная точка навыка
Бот определяет конечную точку, которая перенаправляет входящие активности навыков корневому боту для обработки навыков.
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
@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)
Регистрация сервиса
Добавьте объект конфигурации проверки подлинности со всеми необходимыми проверками утверждений, а также любые дополнительные объекты.
В этом примере используется та же логика конфигурации проверки подлинности для проверки действий как пользователей, так и навыков.
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
/**
* 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)
Тестируйте основного бота
Вы можете протестировать бота, использующего навык, в эмуляторе так же, как и обычного бота; однако вы должны запускать и навык, и бота, использующего навык, одновременно.
Сведения о настройке навыка см. в статье о реализации навыка.
Скачайте и установите последнюю версию Bot Framework Emulator.
- Запустите бот навыка 'Эхо' и простой базовый бот на локальном компьютере. Если вам нужны инструкции, ознакомьтесь с файлом
README для примера C#, JavaScript, JavaScript или Python.
- Примените эмулятор для тестирования бота, как показано ниже. При отправке сообщения
end или stop навыку, навык отправляет корневому боту endOfConversation активность в дополнение к сообщению ответа. Свойство endOfConversation активности указывает, что умение завершилось успешно.
Дополнительные сведения об отладке
Так как трафик между навыками и потребителями навыков проходит проверку подлинности, при отладке таких ботов выполняются дополнительные действия.
- Потребитель навыка и все навыки, которые он потребляет, прямо или косвенно, должны работать.
- Если боты работают локально и если у любого из ботов есть идентификатор приложения и пароль, все боты должны иметь действительные идентификаторы и пароли.
- Если боты развернуты, узнайте, как отладить бота из любого канала с помощью devtunnel.
- Если некоторые боты выполняются локально, а некоторые развертываются, см. инструкции по отладке навыка или потребителя навыков.
В противном случае, вы можете отладить потребителя навыка или сам навык так же, как отлаживаете других ботов. Дополнительные сведения см. в статье отладка бота и отладка с помощью эмулятора Bot Framework.
Ниже приведены некоторые аспекты, которые нужно учитывать при реализации более сложного корневого бота.
Чтобы пользователь мог отменить многоэтапный навык
Корневой бот должен проверять сообщение от пользователя, прежде чем перенаправить его активному модулю навыков. Если пользователь хочет отменить текущий процесс, корневой бот может отправить в навык действие endOfConversation, вместо пересылки сообщения.
Для обмена данными между корневым ботом и навыком
Чтобы отправить параметры этому навыку, потребитель навыка может задать свойство value для сообщений, отправляемых в этот навык. Чтобы получить возвращаемые значения от навыка, потребитель должен проверять свойство value при получении действия endOfConversation от навыка.
Использование нескольких навыков
- Если навык уже активен, корневой бот должен определить активный навык и перенаправить сообщение пользователя в нужный навык.
- Если активных навыков нет, корневой бот должен определить навык для запуска (при его наличии), используя сведения о состоянии бота и сообщении пользователя.
- Если вы хотите, чтобы пользователь мог переключаться между несколькими параллельно выполняющимися навыками, корневой бот должен определять, с каким из активных навыков намерен взаимодействовать пользователь, прежде чем перенаправлять сообщение от пользователя.
Чтобы использовать режим доставки, предполагающий ожидание ответов
Чтобы использовать режим доставки ожидаемых ответов :
- Клонируйте действие из контекста поворота.
-
Задайте для свойства 'режим доставки' нового действия значение "ExpectReplies" прежде чем отправить действие от корневого бота к навыку.
- Чтение ожидаемых ответов из текста ответа вызова , возвращенного из ответа запроса.
- Обработайте каждое действие в корневом боте или отправив его в канал, инициирующий исходный запрос.
Ожидается, что ответы могут быть полезны в ситуациях, когда бот, который отвечает на действие, должен быть тем же экземпляром бота, который получил действие.