다음을 통해 공유


Direct Line을 사용하여 나만의 맞춤 채널 통합

Dynamics 365 Contact Center를 사용하면 .NET SDK의 일부인 Direct Line API 3.0을 사용하여 사용자 지정 메시징 채널을 통합하는 커넥터를 구현할 수 있습니다. 전체 샘플 코드는 사용자 고유의 커넥터를 만드는 방법을 보여 줍니다. Direct Line API 3.0에 대한 자세한 내용은 Direct Line 3.0 API의 주요 개념을 참조하세요.

이 문서에서는 Dynamics 365 Contact Center에 내부적으로 연결된 Microsoft Direct Line Bot Framework에 채널이 연결되는 방법을 설명합니다. 다음 섹션에는 Direct Line API 3.0을 사용하여 Direct Line 클라이언트와 IChannelAdapter 샘플 커넥터를 빌드하는 인터페이스를 만드는 코드 조각이 포함되어 있습니다.

비고

소스 코드 및 설명서에서는 채널이 Direct Line을 통해 Dynamics 365 Contact Center에 연결하는 방법의 전체 흐름을 설명하고 안정성 및 확장성의 측면에 초점을 맞추지 않습니다.

구성 요소

어댑터 웹훅 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 .

인바운드 활동 처리

채널 어댑터는 다음과 같은 인바운드 작업을 수행합니다.

  1. 인바운드 메시지 요청 서명의 유효성을 검사합니다.

채널의 인바운드 요청은 서명 키를 기반으로 유효성이 검사됩니다. 요청이 유효하지 않으면 "잘못된 서명" 예외 메시지가 발생합니다. 요청이 유효한 경우 다음과 같이 진행됩니다.

  /// <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);
  }
  1. 인바운드 요청을 봇 활동으로 변환합니다.

인바운드 요청 페이로드는 Bot Framework가 이해할 수 있는 활동으로 변환됩니다.

비고

작업 페이로드는 메시지 크기 제한인 28KB를 초과해서는 안 됩니다.

이 Activity 개체에는 다음과 같은 특성이 포함됩니다.

특성 설명
이후 사용자 및 이름의 고유 식별자(이름과 성의 조합, 공백 구분 기호로 구분)로 구성된 채널 계정 정보를 저장합니다.
채널 ID 채널 식별자를 나타냅니다. 인바운드 요청의 경우 채널 ID는 directline입니다.
serviceUrl 서비스 URL을 나타냅니다. 인바운드 요청의 경우 서비스 URL은 https://directline.botframework.com/입니다.
유형 활동 유형을 나타냅니다. 메시지 활동의 경우 유형은 message입니다.
문자 메시지 메시지 내용을 저장합니다.
아이디 어댑터가 아웃바운드 메시지에 응답하는 데 사용하는 식별자를 나타냅니다.
채널데이터 , channelType, 및 conversationcontext로 구성된 customercontext채널 데이터를 나타냅니다.
채널유형 고객이 메시지를 보내는 데 사용하는 채널 이름을 나타냅니다. 예: MessageBird, KakaoTalk, Snapchat
대화컨텍스트 작업 스트림에 정의된 컨텍스트 변수를 보유하는 사전 개체를 참조합니다. Dynamics 365 Contact Center는 이 정보를 사용하여 대화를 올바른 고객 서비스 담당자(서비스 담당자 또는 담당자)에게 라우팅합니다. 다음은 그 예입니다.
"conversationcontext ":{ "제품명": "Xbox", "문제":"설치" }
이 예제에서 컨텍스트는 Xbox 설치를 처리하는 서비스 담당자에게 대화를 라우팅합니다.
customercontext 전화 번호 및 이메일 주소와 같은 고객 세부 정보를 포함하는 사전 개체를 나타냅니다. Dynamics 365 Contact Center는 이 정보를 사용하여 사용자의 연락처 레코드를 식별합니다.
"customercontext":{ "이메일":email@email.com, "전화번호":"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"           
        }
    }
}

  1. 메시지 릴레이 프로세서로 활동을 보냅니다.

활동 페이로드를 빌드한 후 메시지 릴레이 프로세서의 PostActivityAsync 메서드를 호출하여 활동을 Direct Line으로 보냅니다. 또한 채널 어댑터는 Dynamics 365 Contact Center에서 직접 회선을 통해 아웃바운드 메시지를 받을 때 릴레이 프로세서가 호출하는 이벤트 처리기를 전달해야 합니다.

아웃바운드 활동 처리

릴레이 프로세서는 이벤트 처리기를 호출하여 아웃바운드 작업을 해당 채널 어댑터로 보내고 어댑터는 아웃바운드 작업을 처리합니다. 채널 어댑터는 다음과 같은 아웃바운드 작업을 수행합니다.

  1. 아웃바운드 활동을 채널 응답 모델로 변환합니다.

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();
  }
  1. 채널 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);
          }
      }
  }

메시지 릴레이 프로세서

메시지 릴레이 프로세서는 채널 어댑터에서 인바운드 활동을 수신하고 활동 모델 유효성 검증을 수행합니다. 릴레이 프로세서는 이 활동을 직접 회선으로 보내기 전에 대화가 특정 활동에 대해 활성 상태인지 확인합니다.

대화가 활성 상태인지 여부를 조회하기 위해 릴레이 프로세서는 사전에 활성 대화 컬렉션을 유지 관리합니다. 이 사전에는 사용자 ID로 키가 포함되어 있으며, 이는 사용자와 Value를 다음 클래스의 개체로 고유하게 식별합니다.

 /// <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; }
}

릴레이 프로세서에서 받은 활동에 대해 대화가 활성화되지 않은 경우 다음 단계를 수행합니다.

  1. 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();
     }
 }
  1. 구성 파일에 구성된 폴링 간격에 따라 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);
    }
}

비고

메시지를 받는 코드의 핵심은 and ConversationId 를 매개 변수로 사용하는 watermark GetActivitiesAsync 메서드입니다. 매개 변수의 watermark 목적은 Direct Line에서 배달되지 않는 메시지를 검색하는 것입니다. watermark 매개 변수를 지정하면 대화가 워터마크에서 재생되므로 메시지가 손실되지 않습니다.

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에서 대화 종료를 참조하세요.

사용자 지정 채널의 Markdown 형식

Direct Line API 3.0을 사용하여 사용자 지정 메시징 채널에서 Markdown으로 서식이 지정된 메시지를 보내고 받을 수 있습니다. Markdown 형식이 채널을 통해 전달되는 방식을 이해하고 형식의 세부 정보를 알면 사용자 인터페이스에서 HTML 스타일 및 태그를 업데이트하는 데 도움이 됩니다.

Direct Line 채널에서 고객 서비스 담당자(서비스 담당자 또는 담당자)가 Markdown으로 형식화된 메시지를 Direct Line 봇에게 보낼 때, 해당 봇은 특정 형식으로 메시지를 수신합니다. 이제 봇이 고객으로부터 형식이 지정된 메시지를 수신(인바운드)하는 경우 Markdown으로 형식이 지정된 메시지를 올바르게 해석할 수 있어야 합니다. 개발자는 Markdown을 적절하게 사용하여 메시지의 형식이 서비스 담당자 및 고객을 위해 올바르게 지정되도록 해야 합니다.

채팅 메시지에 대한 Markdown 형식에 대해 자세히 알아봅니다.

비고

  • 현재 Shift + Enter 키 조합을 사용하여 여러 줄 바꿈을 추가하는 기능은 지원되지 않습니다.
  • 인바운드 메시지의 경우 Markdown 텍스트를 활동 개체의 text 속성으로 설정합니다.
  • 아웃바운드 메시지의 경우 Markdown 텍스트는 작업 개체의 text 속성(일반 메시지와 유사)에서 수신됩니다.

다음 단계

라이브 채팅과 비동기 채널 지원

사용자 지정 메시징 채널 구성
MessageBird API 참조
봇 구성을 위한 모범 사례