Partager via


Intégrer votre propre canal personnalisé à l’aide de Direct Line

Omnicanal pour Customer Service contient une suite de capacités qui étendent la puissance de Dynamics 365 Customer Service Enterprise pour permettre aux organisations de se connecter et d’interagir instantanément avec leurs clients sur les canaux de messagerie numérique. Une licence supplémentaire est requise pour accéder à Omnichannel pour Customer Service. Pour plus d’informations, voir les pages Vue d’ensemble de la tarification de Dynamics 365 Customer Service et Plan de tarification de Dynamics 365 Customer Service.

Avec Omnicanal pour Customer Service, vous pouvez implémenter un connecteur pour intégrer des canaux de messagerie personnalisés en utilisant l’API Direct Line 3.0, qui fait partie du SDK .NET. L’exemple de code complet illustre comment vous pouvez créer votre propre connecteur. Pour en savoir plus sur Direct Line API 3.0, voir Concepts clés dans Direct Line API 3.0.

Cet article explique comment connecter un canal à Microsoft Direct Line Bot Framework, qui est attaché en interne à Omnicanal pour Customer Service. La section suivante comprend des extraits de code qui utilisent Direct Line API 3.0 pour créer un abonné Direct Line et l’interface IChannelAdapter pour créer un exemple de connecteur.

Note

Le code source et la documentation décrivent le flux global de la procédure de connexion du canal à Omnicanal pour Customer Service avec Direct Line, et ne sont pas axés sur les aspects de fiabilité et d’évolutivité.

Composants

Service API Webhook de l’adaptateur

Lorsque l’utilisateur saisit un message, l’API de l’adaptateur est appelée à partir du canal. Elle traite la demande entrante et envoie un statut de réussite ou d’échec en retour. Le service API de l’adaptateur doit implémenter l’interface IChannelAdapter et envoie la demande entrante à l’adaptateur de canal respectif pour traiter la demande.

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

Adaptateurs de canal

L’adaptateur de canal traite les activités entrantes et sortantes et doit implémenter l’interface IAdapterBuilder.

Traiter les activités entrantes

L’adaptateur de canal effectue les activités entrantes suivantes :

  1. Validez la signature de la demande de message entrante.

La requête entrante du canal est validée en fonction de la clé de signature. Si la requête est invalide, un message d’exception « signature invalide » est émis. Si la requête est valide, elle passe aux étapes suivantes :

  /// <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. Convertissez la demande entrante en activité de bot.

La charge utile de la requête entrante est convertie en activité compréhensible par Bot Framework.

Note

La charge utile de l’activité ne doit pas dépasser la limite de taille de message de 28 Ko.

Cet objet Activité comprend les attributs suivants :

Attribute Description
from Stocke les informations du compte du canal, qui se composent de l’Identificateur unique de l’utilisateur et du nom (combinaison de prénom et nom, séparés par un délimiteur d’espace).
channelId Indique l’identificateur du canal. Pour les demandes entrantes, l’ID de canal est directline.
serviceUrl Indique l’URL du service. Pour les demandes entrantes, l’URL du service est https://directline.botframework.com/.
type Indique le type d’activité. Pour les activités de message, le type est message.
texte Stocke le contenu du message.
id Indique l’identificateur que l’adaptateur utilise pour répondre aux messages sortants.
channelData Indique les données du canal qui se composent de channelType, conversationcontext, et customercontext.
channelType Indique le nom du canal par lequel le client envoie des messages. Par exemple, MessageBird, KakaoTalk, Snapchat
conversationcontext Fait référence à un objet de dictionnaire avec les variables de contexte définies dans le flux de travail. Omnicanal pour Customer Service utilise ces informations pour acheminer la conversation vers le bon agent. Par exemple :
"conversationcontext ":{ "ProductName" : "Xbox", "Issue":"Installation" }
Dans cet exemple, le contexte achemine la conversation vers l’agent qui s’occupe de l’installation Xbox.
customercontext Fait référence à un objet de dictionnaire avec les détails du client tels que le numéro de téléphone et l’adresse e-mail. Omnicanal pour Customer Service utilise ces informations pour identifier l’enregistrement de contact de l’utilisateur.
"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;
  }

L’exemple de charge utile JSON est le suivant :

{
    "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. Envoyez l’activité au processeur de relais de messages.

Après avoir créé la charge utile de l’activité, il appelle la méthode PostActivityAsync du processeur de relais de messages pour envoyer l’activité à Direct Line. L’adaptateur de canal doit également transmettre le gestionnaire d’événements, que le processeur de relais appelle lorsqu’il reçoit un message sortant d’Omnicanal pour Customer Service au moyen de Direct Line.

Traiter les activités sortantes

Le processeur de relais appelle le gestionnaire d’événements pour envoyer des activités sortantes à l’adaptateur de canal respectif, et l’adaptateur traite ensuite les activités sortantes. L’adaptateur de canal effectue les activités sortantes suivantes :

  1. Convertissez les activités sortantes vers le modèle de réponse de canal.

Les activités Direct Line sont converties selon le modèle de réponse spécifique au 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. Envoyez vos réponses via le canal API REST.

L’adaptateur de canal appelle l’API REST pour envoyer une réponse sortante au canal, qui est ensuite envoyée à l’utilisateur.

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

Processeur de relais de messages

Le processeur de relais de messages reçoit l’activité entrante de l’adaptateur de canal et effectue la validation du modèle d’activité. Avant d’envoyer cette activité à Direct Line, le processeur de relais vérifie si la conversation est active pour l’activité donnée.

Pour vérifier si la conversation est active, le processeur de relais conserve une collection de conversations actives dans un dictionnaire. Ce dictionnaire contient la clé en tant qu’ID utilisateur qui identifie de manière unique l’utilisateur et la valeur en tant qu’objet de la classe suivante :

 /// <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 conversation n’est pas active pour l’activité reçue par le processeur de relais, il effectue la procédure suivante :

  1. Démarre une conversation avec Direct Line et stocke l’objet de conversation envoyé par Direct Line en fonction de l’ID utilisateur dans le dictionnaire.
 /// <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. Démarre un nouveau thread pour interroger les activités sortantes du bot Direct Line, en fonction de l’intervalle d’interrogation défini dans le fichier de configuration. Le thread d’interrogation est actif jusqu’à ce que la réception de fin de l’activité de conversation par 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

Au cœur du code qui reçoit le message se trouve la méthode GetActivitiesAsync qui prend ConversationId et watermark comme paramètres. Le but du paramètre watermark est de récupérer uniquement les messages qui ne sont pas encore remis par Direct Line. Si le paramètre de filigrane est spécifié, la conversation est relue à partir du filigrane, afin qu’aucun message ne soit perdu.

Envoyer l’activité à 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 conversation est active pour l’activité reçue par le processeur de relais, il envoie l’activité au processeur de relais de message.

Mettre fin à une conversation

Pour mettre fin à la conversation, consultez Terminer une conversation dans Direct Line.

Étapes suivantes

Prise en charge de la conversation instantanée en direct et des canaux asynchrones
Formats Markdown dans les canaux personnalisés qui utilisent Direct Line

Voir aussi

Configurer le canal de la messagerie personnalisée
Référence de l’API MessageBird
Bonnes pratiques pour la configuration des bots