Compartir vía


Integrar el propio canal personalizado con Direct Line

Con la Plataforma omnicanal para Customer Service, puede implementar un conector para integrar canales de mensajería personalizados usando la API Direct Line 3.0, que es parte de .NET SDK. El código de muestra completo ilustra cómo puede crear su propio conector. Para aprender más sobre la API 3.0 de Direct Line, consulte Conceptos clave en la API 3.0 de Direct Line.

Este artículo explica cómo se conecta un canal a Microsoft Direct Line Bot Framework, que se adjunta internamente a Plataforma omnicanal para Customer Service. La siguiente sección incluye fragmentos de código que utilizan la API 3.0 de Direct Line para crear un cliente de Direct Line y la interfaz IChannelAdapter para construir un conector de muestra.

Nota

El código fuente y la documentación describen el flujo general de cómo el canal puede conectarse a Plataforma omnicanal para Customer Service a través de Direct Line y no centrarse en aspectos de confiabilidad y escalabilidad.

Componentes

Servicio de API de Webhook de adaptador

Cuando el usuario ingresa un mensaje, la API del adaptador se invoca desde el canal. Procesa la solicitud entrante y envía un estado de éxito o error como respuesta. El servicio de API del adaptador debe implementar la interfaz IChannelAdapter y envía la solicitud entrante al adaptador de canal respectivo para procesar la solicitud.

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

Adaptadores de canal

El adaptador de canal procesa las actividades entrantes y salientes y debe implementar la interfaz IAdapterBuilder.

Procesar actividades entrantes

El adaptador de canal realiza las siguientes actividades entrantes:

  1. ValidAr la firma de solicitud de mensaje entrante.

La solicitud entrante del canal se valida en función de la clave de firma. Si la solicitud no es válida, se lanza un mensaje de excepción de "firma no válida". Si la solicitud es válida, continúa de esta manera:

  /// <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. Convierta la solicitud entrante en una actividad bot.

La carga útil de la solicitud entrante se convierte en una actividad que el Bot Framework puede entender.

Nota

La carga útil de la actividad no debe exceder el límite de tamaño del mensaje de 28 KB.

Este objeto Actividad incluye los siguientes atributos:

Attribute Description
from Almacena la información de la cuenta del canal, que consiste en el identificador único del usuario y el nombre (combinación de nombre de pila y apellido, separados por un delimitador de espacio).
channelId Indica el identificador del canal. Para las solicitudes entrantes, el ID del canal es directline.
serviceUrl Indica la URL del servicio. Para las solicitudes entrantes, la URL de servicio es https://directline.botframework.com/.
type Indica el tipo de actividad. Para actividades de mensajes, el tipo es message.
texto Almacena el contenido del mensaje.
id Indica el identificador que utiliza el adaptador para responder a los mensajes salientes.
channelData Indica los datos del canal, que constan de channelType, conversationcontext y customercontext.
channelType Indica el nombre del canal a través del cual el cliente envía mensajes. Por ejemplo, MessageBird, KakaoTalk, Snapchat
conversationcontext Se refiere a un objeto de diccionario que contiene las variables de contexto definidas en el flujo de trabajo. Plataforma omnicanal para Customer Service utiliza esta información para enrutar la conversación al agente adecuado. Por ejemplo:
"conversationcontext ":{ "Nombre de producto" : "Xbox", "Incidencia":"Instalación" }
En este ejemplo, el contexto dirige la conversación al agente que se ocupa de la instalación de Xbox.
customercontext Se refiere a un objeto de diccionario que contiene los detalles del cliente, como el número de teléfono y la dirección de correo electrónico. Plataforma omnicanal para Customer Service utiliza esta información para identificar el registro de contacto del usuario.
"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;
  }

La carga útil JSON de muestra es la siguiente:

{
    "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. Envíe la actividad al procesador de retransmisión de mensajes.

Después de construir la carga útil de la actividad, llama al método PostActivityAsync del procesador de retransmisión de mensajes para enviar la actividad a Direct Line. El adaptador de canal también debe pasar el controlador de eventos, que el procesador de retransmisión invocará cuando reciba un mensaje saliente de la Plataforma omnicanal para Customer Service a través de Direct Line.

Procesar actividades salientes

El procesador de retransmisión invoca al controlador de eventos para enviar actividades salientes al adaptador de canal respectivo, y el adaptador procesa las actividades salientes. El adaptador de canal realiza las siguientes actividades salientes:

  1. Convertir las actividades salientes al modelo de respuesta del canal.

Las actividades de Direct Line se convierten al modelo de respuesta específico del canal.

  /// <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. Enviar respuestas a través de la API de REST del canal.

El adaptador de canal llama a la API de REST para enviar una respuesta saliente al canal, que luego se envía al usuario.

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

Procesador de retransmisión de mensajes

El procesador de retransmisión de mensajes recibe la actividad entrante del adaptador de canal y realiza la validación del modelo de actividad. Antes de enviar esta actividad a Direct Line, el procesador de retransmisión comprueba si la conversación está activa para la actividad en particular.

Para consultar si la conversación está activa, el procesador de retransmisión mantiene una colección de conversaciones activas en un diccionario. Este diccionario contiene la clave como id. de usuario que identifica de forma exclusiva al usuario y Valor como un objeto de la siguiente clase:

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

Si la conversación no está activa para la actividad recibida por el procesador de retransmisión, realiza los siguientes pasos:

  1. Inicia una conversación con Direct Line y almacena el objeto de conversación enviado por Direct Line frente al id. de usuario en el diccionario.
 /// <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. Inicia una nueva conversación para sondear las actividades salientes del bot de Direct Line según el intervalo de sondeo configurado en el archivo de configuración. El hilo de sondeo está activo hasta que se recibe el final de la actividad de conversación de 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);
    }
}

Nota

En el centro del código que recibe el mensaje está el método GetActivityAsync, que toma ConversationId y watermark como parámetros. El propósito del parámetro watermark es recuperar los mensajes que aún no se han entregado por Direct Line. Si se especifica el parámetro de marca de agua, la conversación se reproduce desde la marca de agua, por lo que no se pierden mensajes.

Envía la actividad a 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);
 }

Si la conversación está activa para la actividad recibida por el procesador de retransmisión, envía la actividad al procesador de retransmisión de mensajes.

Finalizar una conversación

Para finalizar la conversación, consulte Terminar una conversación en Direct Line.

Pasos siguientes

Soporte para chat en vivo y canales asincrónicos
Formatos de Markdown en canales personalizados que utilizan Direct Line

Consulte también

Configurar el canal de mensajería personalizado
Referencia de API de MessageBird
Prácticas recomendadas para la configuración de bots