Direct Line を使用して独自のカスタム チャネルを統合する
Customer Service 用オムニチャネルでは、.NET SDK の一部である Direct Line API 3.0 を使用して、カスタム メッセージングチャネルを統合するコネクタを実装することができます。 完全な サンプルコード は、独自のコネクタを作成する方法を示しています。 Direct Line API 3.0 の詳細については、Direct Line 3.0 API の重要な概念を参照してください。
この記事では、Customer Service 用オムニチャネル に内部的に接続されている Microsoft Direct Line Bot Framework にチャネルを接続する方法について説明しています。 次のセクションでは、Direct Line の API 3.0 を使用して Direct Line のクライアントを作成し、IChannelAdapter
のインターフェースを使用してサンプル コネクターを構築するコード スニペットを紹介します。
Note
ソースコードとドキュメントは、チャネルが Direct Line を介して Customer Service 用オムニチャネルに接続する方法の全体的な流れを説明するものであり、信頼性やスケーラビリティの側面には焦点を当てていません。
コンポーネント
アダプターWebhook API サービス
ユーザーがメッセージを入力すると、チャネルからアダプター API が呼び出されます。 インバウンド リクエストを処理し、成功または失敗のステータスを応答として送信します。 アダプタ API サービスは、IChannelAdapter
インターフェースを実装する必要があり、インバウンド リクエストをそれぞれのチャネル アダプタに送信し、リクエストを処理します。
/// <summary>
/// Accept an incoming web-hook request from MessageBird Channel
/// </summary>
/// <param name="requestPayload">Inbound request Object</param>
/// <returns>Executes the result operation of the action method asynchronously.</returns>
[HttpPost("postactivityasync")]
public async Task<IActionResult> PostActivityAsync(JToken requestPayload)
{
if (requestPayload == null)
{
return BadRequest("Request payload is invalid.");
}
try
{
await _messageBirdAdapter.ProcessInboundActivitiesAsync(requestPayload, Request).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError($"postactivityasync: {ex}");
return StatusCode(500, "An error occured while handling your request.");
}
return StatusCode(200);
}
チャネル アダプター
チャネル アダプターは、インバウンドおよびアウトバウンド アクティビティを処理し、IAdapterBuilder
インターフェイスを実装する必要があります。
インバウンド活動の処理
チャネル アダプターは、次のインバウンド アクティビティを実行します。
- インバウンド メッセージリクエストの署名を検証します。
チャネルからのインバウンド リクエストは、署名キーに基づいて検証されます。 要求が無効な場合、"無効な署名" 例外メッセージがスローされます。 リクエストが有効な場合、次のように進みます:
/// <summary>
/// Validate Message Bird Request
/// </summary>
/// <param name="content">Request Content</param>
/// <param name="request">HTTP Request</param>
/// <param name="messageBirdSigningKey">Message Bird Signing Key</param>
/// <returns>True if there request is valid, false if there aren't.</returns>
public static bool ValidateMessageBirdRequest(string content, HttpRequest request, string messageBirdSigningKey)
{
if (string.IsNullOrWhiteSpace(messageBirdSigningKey))
{
throw new ArgumentNullException(nameof(messageBirdSigningKey));
}
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
if (string.IsNullOrWhiteSpace(content))
{
throw new ArgumentNullException(nameof(content));
}
var messageBirdRequest = new MessageBirdRequest(
request.Headers?["Messagebird-Request-Timestamp"],
request.QueryString.Value?.Equals("?",
StringComparison.CurrentCulture) != null
? string.Empty
: request.QueryString.Value,
GetBytes(content));
var messageBirdRequestSigner = new MessageBirdRequestSigner(GetBytes(messageBirdSigningKey));
string expectedSignature = request.Headers?["Messagebird-Signature"];
return messageBirdRequestSigner.IsMatch(expectedSignature, messageBirdRequest);
}
- 受信リクエストをボットの活動に変換します。
インバウンド リクエストのペイロードは、Bot Framework が理解可能なアクティビティに変換されます。
Note
アクティビティ ペイロードは、メッセージ サイズの制限である 28 KB を超えてはなりません。
この活動オブジェクトには、次の属性が含まれています。
Attribute | 説明設定 |
---|---|
from | ユーザーの一意識別子と名前 (姓と名の組み合わせをスペースで区切ったもの) を含んだチャネルのアカウント情報を格納します。 |
channelId | チャンネルの識別子を示します。 インバウンド リクエストの場合、チャネル ID は directline です。 |
serviceUrl | サービスの URL を示します。 インバウンド リクエストの場合、サービスの URL は https://directline.botframework.com/ です。 |
タイプ | 活動の種類を示します。 メッセージ活動の場合、この種類は message です。 |
text | メッセージの内容を格納します。 |
ID | アダプターがアウトバウンド メッセージに応答する際に使用する識別子を示します。 |
channelData | channelType 、conversationcontext 、customercontext で構成されたチャネル データを示します。 |
channelType | 顧客がメッセージを送信する際に使用するチャネル名を示します。 たとえば、MessageBird、KakaoTalk、SnapChat です |
conversationcontext | ワークストリームで定義されたコンテキスト変数を保持するディクショナリ オブジェクトを参照します。 Customer Service 用オムニチャネルはこの情報を使用して、会話を適切なエージェントにルーティングします。 たとえば、次のようなものです。 「会話コンテキスト」:{ "商品名":" Xbox "、"問題 ":"インストール " } この例では、コンテキストでは、Xbox のインストールを担当するエージェントに会話が転送されます。 |
customercontext | 電話番号やメールアドレスなどの顧客の詳細を保持する辞書オブジェクトを参照します。 Customer Service 用オムニチャネルは、この情報を使用してユーザーの連絡先レコードを識別します。 "customercontext":{ "email":email@email.com, "phonenumber":"1234567890" } |
/// <summary>
/// Build Bot Activity type from the inbound MessageBird request payload<see cref="Activity"/>
/// </summary>
/// <param name = "messagePayload"> Message Bird Activity Payload</param>
/// <returns>Direct Line Activity</returns>
public static Activity PayloadToActivity(MessageBirdRequestModel messagePayload)
{
if (messagePayload == null)
{
throw new ArgumentNullException(nameof(messagePayload));
}
if (messagePayload.Message?.Direction == ConversationMessageDirection.Sent ||
messagePayload.Type == ConversationWebhookMessageType.MessageUpdated)
{
return null;
}
var channelData = new ActivityExtension
{
ChannelType = ChannelType.MessageBird,
// Add Conversation Context in below dictionary object. Please refer the document for more information.
ConversationContext = new Dictionary<string, string>(),
// Add Customer Context in below dictionary object. Please refer the document for more information.
CustomerContext = new Dictionary<string, string>()
};
var activity = new Activity
{
From = new ChannelAccount(messagePayload.Message?.From, messagePayload.Contact?.DisplayName),
Text = messagePayload.Message?.Content?.Text,
Type = ActivityTypes.Message,
Id = messagePayload.Message?.ChannelId,
ServiceUrl = Constant.DirectLineBotServiceUrl,
ChannelData = channelData
};
return activity;
}
サンプルの JSON ペイロードは次のとおりです:
{
"type": "message",
"id": "bf3cc9a2f5de...",
"serviceUrl": https://directline.botframework.com/,
"channelId": "directline",
"from": {
"id": "1234abcd",// userid which uniquely identify the user
"name": "customer name" // customer name as First Name <space> Last Name
},
"text": "Hi,how are you today.",
"channeldata":{
"channeltype":"messageBird",
"conversationcontext ":{ // this holds context variables defined in Workstream
"ProductName" : "XBox",
"Issue":"Installation"
},
"customercontext":{
"email":email@email.com,
"phonenumber":"1234567890"
}
}
}
- メッセージ リレー プロセッサに活動を送信します。
活動のペイロードを構築した後、メッセージリレープロセッサの PostActivityAsync メソッドを呼び出し、Direct Line にアクティビティを送信します。 また、チャネルアダプタは、Customer Service 用オムニチャネルから Direct Line 経由でアウトバウンド メッセージを受信したときにリレー プロセッサが呼び出すイベント ハンドラを渡す必要があります。
アウトバウンド活動の処理
リレープロセッサはイベントハンドラを呼び出して、アウトバウンド活動をそれぞれのチャネル アダプタに送信し、アダプタはアウトバウンド活動を処理します。 チャネル アダプターは、以下のアウトバウンド活動を行います:
- アウトバウンド活動をチャネル応答モデルに変換します。
Direct Line の活動は、チャネル固有の応答モデルに変換されます。
/// <summary>
/// Creates MessageBird response object from a Bot Framework <see cref="Activity"/>.
/// </summary>
/// <param name="activities">The outbound activities.</param>
/// <param name="replyToId">Reply Id of Message Bird user.</param>
/// <returns>List of MessageBird Responses.</returns>
public static List<MessageBirdResponseModel> ActivityToMessageBird(IList<Activity> activities, string replyToId)
{
if (string.IsNullOrWhiteSpace(replyToId))
{
throw new ArgumentNullException(nameof(replyToId));
}
if (activities == null)
{
throw new ArgumentNullException(nameof(activities));
}
return activities.Select(activity => new MessageBirdResponseModel
{
To = replyToId,
From = activity.ChannelId,
Type = "text",
Content = new Content
{
Text = activity.Text
}
}).ToList();
}
- チャネルの REST API を介して応答を送信します。
チャネル アダプタは REST API を呼び出して、チャネルにアウトバウンド応答を送信し、それがユーザーに送信されます。
/// <summary>
/// Send Outbound Messages to Message Bird
/// </summary>
/// <param name="messageBirdResponses">Message Bird Response object</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task SendMessagesToMessageBird(IList<MessageBirdResponseModel> messageBirdResponses)
{
if (messageBirdResponses == null)
{
throw new ArgumentNullException(nameof(messageBirdResponses));
}
foreach (var messageBirdResponse in messageBirdResponses)
{
using (var request = new HttpRequestMessage(HttpMethod.Post, $"{MessageBirdDefaultApi}/send"))
{
var content = JsonConvert.SerializeObject(messageBirdResponse);
request.Content = new StringContent(content, Encoding.UTF8, "application/json");
await _httpClient.SendAsync(request).ConfigureAwait(false);
}
}
}
メッセージ リレー プロセッサ
メッセージ リレー プロセッサは、チャネル アダプタからインバウンド活動を受信し、アクティビティ モデルの検証を行います。 このアクティビティを Direct Line に送信する前に、リレー プロセッサは、特定の活動に対して会話がアクティブであるかどうかをチェックします。
会話がアクティブかどうかを調べる目的で、リレー プロセッサはアクティブな会話のコレクションを辞書に保持します。 このディクショナリには、ユーザーを一意に識別するユーザー ID としてのキーと、次のクラスのオブジェクトとしての値が含まれています:
/// <summary>
/// Direct Line Conversation to store as an Active Conversation
/// </summary>
public class DirectLineConversation
{
/// <summary>
/// .NET SDK Client to connect to Direct Line Bot
/// </summary>
public DirectLineClient DirectLineClient { get; set; }
/// <summary>
/// Direct Line response after start a new conversation
/// </summary>
public Conversation Conversation { get; set; }
/// <summary>
/// Watermark to guarantee that no messages are lost
/// </summary>
public string WaterMark { get; set; }
}
リレー プロセッサが受信した活動に対して会話がアクティブでない場合は、次の手順を実行します:
- Direct Line との会話を開始し、Direct Line が送信した会話オブジェクトをユーザー ID と照合して辞書に格納します。
/// <summary>
/// Initiate Conversation with Direct Line Bot
/// </summary>
/// <param name="inboundActivity">Inbound message from Aggregator/Channel</param>
/// <param name="adapterCallBackHandler">Call Back to send activities to Messaging API</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
private async Task InitiateConversation(Activity inboundActivity, EventHandler<IList<Activity>> adapterCallBackHandler)
{
var directLineConversation = new DirectLineConversation
{
DirectLineClient = new DirectLineClient(_relayProcessorConfiguration.Value.DirectLineSecret)
};
// Start a conversation with Direct Line Bot
directLineConversation.Conversation = await directLineConversation.DirectLineClient.Conversations.
StartConversationAsync().ConfigureAwait(false);
await directLineConversation.DirectLineClient.Conversations.
StartConversationAsync().ConfigureAwait(false);
if (directLineConversation.Conversation == null)
{
throw new Exception(
"An error occurred while starting the Conversation with direct line. Please validate the direct line secret in the configuration file.");
}
// Adding the Direct Line Conversation object to the lookup dictionary and starting a thread to poll the activities from the direct line bot.
if (ActiveConversationCache.ActiveConversations.TryAdd(inboundActivity.From.Id, directLineConversation))
{
// Starts a new thread to poll the activities from Direct Line Bot
new Thread(async () => await PollActivitiesFromBotAsync(
directLineConversation.Conversation.ConversationId, inboundActivity, adapterCallBackHandler).ConfigureAwait(false))
.Start();
}
}
- 構成ファイルで構成されたポーリングの間隔に基づき、新しいスレッドを開始して Direct Line ボットからのアウトバウンド活動をポーリングします。 ポーリングスレッドは、Direct Line から会話終了のアクティビティを受け取るまで有効です。
/// <summary>
/// Polling the activities from BOT for the active conversation
/// </summary>
/// <param name="conversationId">Direct Line Conversation Id</param>
/// <param name="inboundActivity">Inbound Activity from Channel/Aggregator</param>
/// <param name="lineActivitiesReceived">Call Back to send activities to Messaging API</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
private async Task PollActivitiesFromBotAsync(string conversationId, Activity inboundActivity, EventHandler<IList<Activity>> lineActivitiesReceived)
{
if (!int.TryParse(_relayProcessorConfiguration.Value.PollingIntervalInMilliseconds, out var pollingInterval))
{
throw new FormatException($"Invalid Configuration value of PollingIntervalInMilliseconds: {_relayProcessorConfiguration.Value.PollingIntervalInMilliseconds}");
}
if (!ActiveConversationCache.ActiveConversations.TryGetValue(inboundActivity.From.Id,
out var conversationContext))
{
throw new KeyNotFoundException($"No active conversation found for {inboundActivity.From.Id}");
}
while (true)
{
var watermark = conversationContext.WaterMark;
// Retrieve the activity set from the bot.
var activitySet = await conversationContext.DirectLineClient.Conversations.
GetActivitiesAsync(conversationId, watermark).ConfigureAwait(false);
// Set the watermark to the message received
watermark = activitySet?.Watermark;
// Extract the activities sent from our bot.
if (activitySet != null)
{
var activities = (from activity in activitySet.Activities
where activity.From.Id == _relayProcessorConfiguration.Value.BotHandle
select activity).ToList();
if (activities.Count > 0)
{
SendReplyActivity(activities, inboundActivity, lineActivitiesReceived);
}
// Update Watermark
ActiveConversationCache.ActiveConversations[inboundActivity.From.Id].WaterMark = watermark;
if (activities.Exists(a => a.Type.Equals("endOfConversation", StringComparison.InvariantCulture)))
{
if (ActiveConversationCache.ActiveConversations.TryRemove(inboundActivity.From.Id, out _))
{
Thread.CurrentThread.Abort();
}
}
}
await Task.Delay(TimeSpan.FromMilliseconds(pollingInterval)).ConfigureAwait(false);
}
}
Note
メッセージを受信するコードの中心には、ConversationId
と watermark
をパラメーターとして受け取る GetActivitiesAsync メソッドがあります。 watermark
パラメーターの目的は、Direct Line によってまだ配信されていないメッセージを取得することです。 ウォーターマークのパラメーターが指定されている場合、会話はウォーターマークから再生されるため、メッセージは失われません。
Direct Line にアクティビティを送信する
/// <summary>
/// Send the activity to the bot using Direct Line client
/// </summary>
/// <param name="inboundActivity">Inbound message from Aggregator/Channel</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
private static async Task SendActivityToBotAsync(Activity inboundActivity)
{
if (!ActiveConversationCache.ActiveConversations.TryGetValue(inboundActivity.From.Id,
out var conversationContext))
{
throw new KeyNotFoundException($"No active conversation found for {inboundActivity.From.Id}");
}
await conversationContext.DirectLineClient.Conversations.PostActivityAsync(
conversationContext.Conversation.ConversationId, inboundActivity).ConfigureAwait(false);
}
リレー プロセッサが受信した活動に対して会話がアクティブである場合、会話はメッセージリレープロセッサに活動を送信します。
会話の終了
会話を終了するには、Direct Line で会話を終了する を参照してください。
次の手順
ライブ チャットと非同期チャネルのサポート
Direct Line を使用するカスタム チャネルの Markdown 形式
参照
カスタム メッセージング チャネルを構成する
MessageBird API リファレンス
ボットを構成するためのベストプラクティス