{
"MicrosoftAppType": "",
"MicrosoftAppId": "",
"MicrosoftAppPassword": "",
"MicrosoftAppTenantId": "",
// This is a comma separate list with the App IDs that will have access to the skill.
// This setting is used in AllowedCallersClaimsValidator.
// Examples:
// [ "*" ] allows all callers.
// [ "AppId1", "AppId2" ] only allows access to parent bots with "AppId1" and "AppId2".
"AllowedCallers": [ "*" ]
}
필요에 따라 application.properties 파일에 기술의 앱 ID와 암호를 추가합니다.
MicrosoftAppId=
MicrosoftAppPassword=
server.port=39783
# This is a comma separate list with the App IDs that will have access to the skill.
# This setting is used in AllowedCallersClaimsValidator.
# Examples:
# * allows all callers.
# AppId1,AppId2 only allows access to parent bots with "AppId1" and "AppId2".
AllowedCallers=*
필요에 따라 기술의 앱 ID와 암호를 config.py 파일에 추가합니다.
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", "")
# Callers to only those specified, '*' allows any caller.
활동 처리기 논리
입력 매개 변수를 허용하려면
기술 소비자는 기술에 정보를 보낼 수 있습니다. 이러한 정보를 수락하는 한 가지 방법은 들어오는 메시지의 값 속성을 통해 수락하는 것입니다. 또 다른 방법은 이벤트를 처리하고 활동을 호출하는 것입니다.
이 예제의 기술은 입력 매개 변수를 허용하지 않습니다.
대화를 계속하거나 완료하려면
기술이 활동을 보내면 기술 소비자는 활동을 사용자에게 전달해야 합니다.
그러나 기술이 완료되면 활동을 보내야 endOfConversation 합니다. 그렇지 않으면 기술 소비자가 계속해서 사용자 활동을 기술로 전달합니다.
필요한 경우 활동의 value 속성을 사용하여 반환 값을 포함시키고, 활동의 code 속성을 사용하여 기술이 종료되는 이유를 표시합니다.
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
if (turnContext.Activity.Text.Contains("end") || turnContext.Activity.Text.Contains("stop"))
{
// Send End of conversation at the end.
var messageText = $"ending conversation from the skill...";
await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput), cancellationToken);
var endOfConversation = Activity.CreateEndOfConversationActivity();
endOfConversation.Code = EndOfConversationCodes.CompletedSuccessfully;
await turnContext.SendActivityAsync(endOfConversation, cancellationToken);
}
else
{
var messageText = $"Echo: {turnContext.Activity.Text}";
await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput), cancellationToken);
messageText = "Say \"end\" or \"stop\" and I'll end the conversation and back to the parent.";
await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput), cancellationToken);
}
}
echo-skill-bot/bot.js
this.onMessage(async (context, next) => {
switch (context.activity.text.toLowerCase()) {
case 'end':
case 'stop':
await context.sendActivity({
type: ActivityTypes.EndOfConversation,
code: EndOfConversationCodes.CompletedSuccessfully
});
break;
default:
await context.sendActivity(`Echo (JS) : '${ context.activity.text }'`);
await context.sendActivity('Say "end" or "stop" and I\'ll end the conversation and back to the parent.');
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
echoSkillBot\EchoBot.java
protected CompletableFuture<Void> onMessageActivity(TurnContext turnContext) {
if (
turnContext.getActivity().getText().contains("end") || turnContext.getActivity().getText().contains("stop")
) {
String messageText = "ending conversation from the skill...";
return turnContext.sendActivity(MessageFactory.text(messageText, messageText, InputHints.IGNORING_INPUT))
.thenApply(result -> {
Activity endOfConversation = Activity.createEndOfConversationActivity();
endOfConversation.setCode(EndOfConversationCodes.COMPLETED_SUCCESSFULLY);
return turnContext.sendActivity(endOfConversation);
})
.thenApply(finalResult -> null);
} else {
String messageText = String.format("Echo: %s", turnContext.getActivity().getText());
return turnContext.sendActivity(MessageFactory.text(messageText, messageText, InputHints.IGNORING_INPUT))
.thenApply(result -> {
String nextMessageText =
"Say \"end\" or \"stop\" and I'll end the conversation and back to the parent.";
return turnContext.sendActivity(
MessageFactory.text(nextMessageText, nextMessageText, InputHints.EXPECTING_INPUT)
);
})
.thenApply(result -> null);
}
}
echo-skill-bot/bots/echo_bot.py
async def on_message_activity(self, turn_context: TurnContext):
if "end" in turn_context.activity.text or "stop" in turn_context.activity.text:
# Send End of conversation at the end.
await turn_context.send_activity(
MessageFactory.text("Ending conversation from the skill...")
)
end_of_conversation = Activity(type=ActivityTypes.end_of_conversation)
end_of_conversation.code = EndOfConversationCodes.completed_successfully
await turn_context.send_activity(end_of_conversation)
else:
await turn_context.send_activity(
MessageFactory.text(f"Echo (python): {turn_context.activity.text}")
)
await turn_context.send_activity(
MessageFactory.text(
f'Say "end" or "stop" and I\'ll end the conversation and back to the parent.'
)
)
기술을 취소하려면
멀티 턴 기술의 경우 기술 소비자의 활동을 수락 endOfConversation 하여 소비자가 현재 대화를 취소할 수 있도록 합니다.
이 기술에 대한 논리는 차례로 변경되지 않습니다. 대화 리소스를 할당하는 기술을 구현하는 경우 대화 종료 처리기에 리소스 정리 코드를 추가합니다.
protected override Task OnEndOfConversationActivityAsync(ITurnContext<IEndOfConversationActivity> turnContext, CancellationToken cancellationToken)
{
// This will be called if the root bot is ending the conversation. Sending additional messages should be
// avoided as the conversation may have been deleted.
// Perform cleanup of resources if needed.
return Task.CompletedTask;
}
echo-skill-bot/bot.js
메서드를 onUnrecognizedActivityType 사용하여 대화 종료 논리를 추가합니다. 처리기에서 인식할 수 없는 활동이 type 같은지 확인합니다 endOfConversation.
this.onEndOfConversation(async (context, next) => {
// This will be called if the root bot is ending the conversation. Sending additional messages should be
// avoided as the conversation may have been deleted.
// Perform cleanup of resources if needed.
// By calling next() you ensure that the next BotHandler is run.
await next();
});
echoSkillBot\EchoBot.java
protected CompletableFuture<Void> onEndOfConversationActivity(TurnContext turnContext) {
// This will be called if the root bot is ending the conversation. Sending
// additional messages should be
// avoided as the conversation may have been deleted.
// Perform cleanup of resources if needed.
return CompletableFuture.completedFuture(null);
}
echo-skill-bot/bots/echo_bot.py
async def on_end_of_conversation_activity(self, turn_context: TurnContext):
# This will be called if the root bot is ending the conversation. Sending additional messages should be
# avoided as the conversation may have been deleted.
# Perform cleanup of resources if needed.
pass
클레임 유효성 검사기
이 샘플에서는 클레임 유효성 검사에 허용되는 호출자 목록을 사용합니다. 기술의 구성 파일은 목록을 정의합니다. 그런 다음 유효성 검사기 개체가 목록을 읽습니다.
인증 구성에 클레임 유효성 검사기를 추가해야 합니다. 클레임은 인증 헤더 후에 평가됩니다. 유효성 검사 코드는 요청을 거부하는 오류 또는 예외를 throw해야 합니다. 그렇지 않으면 인증된 요청을 거부할 수 있는 여러 가지 이유가 있습니다. 예시:
이 기술은 유료 서비스의 일부입니다. 데이터베이스에 사용자가 없으면 액세스할 수 없습니다.
기술은 독점적입니다. 특정 기술 소비자만 이 기술을 호출할 수 있습니다.
Important
클레임 유효성 검사기를 제공하지 않으면 봇은 기술 소비자로부터 활동을 받으면 오류 또는 예외를 생성합니다.
SDK는 기술을 호출할 수 있는 애플리케이션의 간단한 ID 목록을 기반으로 애플리케이션 수준 권한 부여를 추가하는 클래스를 제공합니다 AllowedCallersClaimsValidator . 목록에 별표(*)가 포함되어 있으면 모든 호출자가 허용됩니다. 클레임 유효성 검사기는 Startup.cs 구성됩니다.
SDK는 기술을 호출할 수 있는 애플리케이션의 간단한 ID 목록을 기반으로 애플리케이션 수준 권한 부여를 추가하는 클래스를 제공합니다 allowedCallersClaimsValidator . 목록에 별표(*)가 포함되어 있으면 모든 호출자가 허용됩니다. 클레임 유효성 검사기는 index.js 구성됩니다.
SDK는 기술을 호출할 수 있는 애플리케이션의 간단한 ID 목록을 기반으로 애플리케이션 수준 권한 부여를 추가하는 클래스를 제공합니다 AllowedCallersClaimsValidator . 목록에 별표(*)가 포함되어 있으면 모든 호출자가 허용됩니다. 클레임 유효성 검사기는 Application.java 구성됩니다.
들어오는 요청을 거부하기 위해 오류를 throw하는 클레임 유효성 검사 메서드를 정의합니다.
class AllowedCallersClaimsValidator:
config_key = "ALLOWED_CALLERS"
def __init__(self, config: DefaultConfig):
if not config:
raise TypeError(
"AllowedCallersClaimsValidator: config object cannot be None."
)
# ALLOWED_CALLERS is the setting in config.py file
# that consists of the list of parent bot ids that are allowed to access the skill
# to add a new parent bot simply go to the AllowedCallers and add
# the parent bot's microsoft app id to the list
caller_list = getattr(config, self.config_key)
if caller_list is None:
raise TypeError(f'"{self.config_key}" not found in configuration.')
self._allowed_callers = frozenset(caller_list)
@property
def claims_validator(self) -> Callable[[List[Dict]], Awaitable]:
async def allow_callers_claims_validator(claims: Dict[str, object]):
# if allowed_callers is None we allow all calls
if "*" not in self._allowed_callers and SkillValidation.is_skill_claim(
claims
):
# Check that the appId claim in the skill request is in the list of skills configured for this bot.
app_id = JwtTokenValidation.get_app_id_from_claims(claims)
if app_id not in self._allowed_callers:
raise PermissionError(
f'Received a request from a bot with an app ID of "{app_id}".'
f" To enable requests from this caller, add the app ID to your configuration file."
)
return
return allow_callers_claims_validator
기술 어댑터
오류가 발생하면 기술 어댑터가 기술에 대한 대화 상태를 지워야 하며 기술 소비자에게도 활동을 보내야 endOfConversation 합니다. 오류로 인해 기술이 종료되었음을 알리려면 활동의 코드 속성을 사용합니다.
private async Task HandleTurnError(ITurnContext turnContext, Exception exception)
{
// Log any leaked exception from the application.
_logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");
await SendErrorMessageAsync(turnContext, exception);
await SendEoCToParentAsync(turnContext, exception);
}
private async Task SendErrorMessageAsync(ITurnContext turnContext, Exception exception)
{
try
{
// Send a message to the user.
var errorMessageText = "The skill 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.
// Note: we return the entire exception in the value property to help the developer;
// this should not be done in production.
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 SendEoCToParentAsync(ITurnContext turnContext, Exception exception)
{
try
{
// Send an EndOfConversation activity to the skill caller with the error to end the conversation,
// and let the caller decide what to do.
var endOfConversation = Activity.CreateEndOfConversationActivity();
endOfConversation.Code = "SkillError";
endOfConversation.Text = exception.Message;
await turnContext.SendActivityAsync(endOfConversation);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Exception caught in SendEoCToParentAsync : {ex}");
}
}
echo-skill-bot/index.js
// 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 a production environment, you should consider logging this to Azure
// application insights.
console.error(`\n [onTurnError] unhandled error: ${ error }`);
await sendErrorMessage(context, error);
await sendEoCToParent(context, error);
};
async function sendErrorMessage(context, error) {
try {
// Send a message to the user.
let onTurnErrorMessage = 'The skill encountered an error or bug.';
await context.sendActivity(onTurnErrorMessage, onTurnErrorMessage, InputHints.ExpectingInput);
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 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.
await context.sendTraceActivity('OnTurnError Trace', error.toString(), 'https://www.botframework.com/schemas/error', 'TurnError');
} catch (err) {
console.error(`\n [onTurnError] Exception caught in sendErrorMessage: ${ err }`);
}
}
async function sendEoCToParent(context, error) {
try {
// Send an EndOfConversation activity to the skill caller with the error to end the conversation,
// and let the caller decide what to do.
const endOfConversation = {
type: ActivityTypes.EndOfConversation,
code: 'SkillError',
text: error.toString()
};
await context.sendActivity(endOfConversation);
} catch (err) {
console.error(`\n [onTurnError] Exception caught in sendEoCToParent: ${ err }`);
}
}
echoSkillBot\SkillAdapterWithErrorHandler.java
public SkillAdapterWithErrorHandler(
Configuration configuration,
AuthenticationConfiguration authenticationConfiguration
) {
super(configuration, authenticationConfiguration);
setOnTurnError(new SkillAdapterErrorHandler());
}
private class SkillAdapterErrorHandler implements OnTurnErrorHandler {
@Override
public CompletableFuture<Void> invoke(TurnContext turnContext, Throwable exception) {
return sendErrorMessage(turnContext, exception).thenAccept(result -> {
sendEoCToParent(turnContext, exception);
});
}
private CompletableFuture<Void> sendErrorMessage(TurnContext turnContext, Throwable exception) {
try {
// Send a message to the user.
String errorMessageText = "The skill 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())
);
}
);
});
} catch (Exception ex) {
return Async.completeExceptionally(ex);
}
}
private CompletableFuture<Void> sendEoCToParent(TurnContext turnContext, Throwable exception) {
try {
// Send an EndOfConversation activity to the skill caller with the error to end
// the conversation,
// and let the caller decide what to do.
Activity endOfConversation = Activity.createEndOfConversationActivity();
endOfConversation.setCode(EndOfConversationCodes.SKILL_ERROR);
endOfConversation.setText(exception.getMessage());
return turnContext.sendActivity(endOfConversation).thenApply(result -> null);
} catch (Exception ex) {
return Async.completeExceptionally(ex);
}
}
}
echo-skill-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._send_eoc_to_parent(turn_context, error)
async def _send_error_message(self, turn_context: TurnContext, error: Exception):
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 _send_eoc_to_parent(self, turn_context: TurnContext, error: Exception):
try:
# Send an EndOfConversation activity to the skill caller with the error to end the conversation,
# and let the caller decide what to do.
end_of_conversation = Activity(type=ActivityTypes.end_of_conversation)
end_of_conversation.code = "SkillError"
end_of_conversation.text = str(error)
await turn_context.send_activity(end_of_conversation)
except Exception as exception:
print(
f"\n Exception caught on _send_eoc_to_parent : {exception}",
file=sys.stderr,
)
traceback.print_exc()
서비스 등록
Bot Framework 어댑터는 인증 구성 개체(어댑터가 만들어질 때 설정됨)를 사용하여 들어오는 요청에서 인증 헤더의 유효성을 검사합니다.
이 샘플에서는 클레임 유효성 검사를 인증 구성에 추가하고, 이전 섹션에서 설명한 오류 처리기가 있는 기술 어댑터를 사용합니다.
options.SerializerSettings.MaxDepth = HttpHelper.BotMessageSerializerSettings.MaxDepth;
});
// Register AuthConfiguration to enable custom claim validation.
services.AddSingleton(sp =>
{
var allowedCallers = new List<string>(sp.GetService<IConfiguration>().GetSection("AllowedCallers").Get<string[]>());
var claimsValidator = new AllowedCallersClaimsValidator(allowedCallers);
// 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
};
});
// Create the Bot Framework Authentication to be used with the Bot Adapter.
services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();
echo-skill-bot/index.js
const allowedCallers = (process.env.AllowedCallers || '').split(',').filter((val) => val) || [];
const claimsValidators = allowedCallersClaimsValidator(allowedCallers);
// If the MicrosoftAppTenantId is specified in the environment config, add the tenant as a valid JWT token issuer for Bot to Skill conversation.
// The token issuer for MSI and single tenant scenarios will be the tenant where the bot is registered.
let validTokenIssuers = [];
const { MicrosoftAppTenantId } = process.env;
if (MicrosoftAppTenantId) {
// For SingleTenant/MSI auth, the JWT tokens will be issued from the bot's home tenant.
// Therefore, these issuers need to be added to the list of valid token issuers for authenticating activity requests.
validTokenIssuers = [
`${ AuthenticationConstants.ValidTokenIssuerUrlTemplateV1 }${ MicrosoftAppTenantId }/`,
`${ AuthenticationConstants.ValidTokenIssuerUrlTemplateV2 }${ MicrosoftAppTenantId }/v2.0/`,
`${ AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV1 }${ MicrosoftAppTenantId }/`,
`${ AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV2 }${ MicrosoftAppTenantId }/v2.0/`
];
}
// Define our authentication configuration.
const authConfig = new AuthenticationConfiguration([], claimsValidators, validTokenIssuers);
const credentialsFactory = new ConfigurationServiceClientCredentialFactory({
MicrosoftAppId: process.env.MicrosoftAppId,
MicrosoftAppPassword: process.env.MicrosoftAppPassword,
MicrosoftAppType: process.env.MicrosoftAppType,
MicrosoftAppTenantId: process.env.MicrosoftAppTenantId
});
const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication(process.env, credentialsFactory, authConfig);
// Create adapter.
// See https://aka.ms/about-bot-adapter to learn more about how bots work.
const adapter = new CloudAdapter(botFrameworkAuthentication);
echoSkillBot\Application.java
@Override
public AuthenticationConfiguration getAuthenticationConfiguration(Configuration configuration) {
AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration();
authenticationConfiguration.setClaimsValidator(
new AllowedCallersClaimsValidator(Arrays.asList(configuration.getProperties(configKey)))
);
return authenticationConfiguration;
}
echo-skill-bot/app.py
CLAIMS_VALIDATOR = AllowedCallersClaimsValidator(CONFIG)
AUTH_CONFIG = AuthenticationConfiguration(
claims_validator=CLAIMS_VALIDATOR.claims_validator
)
# Create adapter.
# See https://aka.ms/about-bot-adapter to learn more about how bots work.
SETTINGS = ConfigurationBotFrameworkAuthentication(
CONFIG,
auth_configuration=AUTH_CONFIG,
)
ADAPTER = AdapterWithErrorHandler(SETTINGS)
기술 매니페스트
기술 매니페스트는 기술이 수행할 수 있는 활동, 입력 및 출력 매개 변수 및 기술의 엔드포인트를 설명하는 JSON 파일입니다.
매니페스트에는 다른 봇의 기술에 액세스하는 데 필요한 정보가 포함됩니다.
최신 스키마 버전은 v2.1입니다.