microsoft / BotFramework-Composer

Dialog creation and management for Microsoft Bot Framework Applications
https://docs.microsoft.com/en-us/composer/
MIT License
871 stars 372 forks source link

Bot entire conversation saving #4257

Closed jintu123 closed 4 years ago

jintu123 commented 4 years ago

I am using my own external DB and I am trying to save the entire conversation which happened with the user to the Backend using My API, But I am unable to pinpoint or find what to write in the end, which variable will be containing the data to the entire conversation in the bot composer.

Can anyone help me with how can i save the entire chat log in bot composer?

dmvtech commented 4 years ago

Hi @jintu123

For future reference, the Microsoft Bot Framework team prefers that how to questions be submitted on Stack Overflow. The official Bot Framework Github repos are the preferred platform for submitting bug fixes and feature requests.

Composer bots have built in support for Inspection Middleware. But that isn't really what you are wanting to do (it's more of a "live" debug view using Emulator). If you're interested, you can configure that in your settings as seen here. Also how this is implemented (in .NET) is seen here.

I think Transcript logging is the more appropriate approach. Unfortunately, there is no way to configure that in Composer. What you would need to do is to modify the actual runtime. Here's a general overview of transcript logging (used in Emulator). Here is info more specific to logging transcripts programmatically (that's what you would need to do). It's specific to blob storage, but you should be able to modify it. Bottom line is that there is no easy way to do it and not through Composer.

dmvtech commented 4 years ago

Hi @jintu123 I'm going to close this as it seems as though i've answered your question.

selvaduraiece commented 3 years ago

Hi Dana,

Is Transcript middleware is supported in Composer now? I can see that new Bot composer is released on December includes orchestrator. Even I saw that Transcript Logger Middleware in the Runtime code image

As you mentioned above I have already configured all those things. Even I can see data, but it holds all the activities, Dialogs, DialogStack etc.. But ideally I wanted only transcript.

Can you help me with steps?

Actual Requirement: Ideally my requirement is: store the chat transcript in the storage while conversation is happening, and retrieve it from the storage and while transferring the chat to live agent, share this retrieved chat transcript as well.

aparnaji commented 2 years ago

@selvaduraiece

Are you able to find a solution for this, I am also looking to extract the transcript alone.

selvaduraiece commented 2 years ago

I built my own middleware to get the TRANSCRIPT in OnTurnAsync activity

alex12pabon commented 2 years ago

Hi @selvaduraiece , could you share your middleware?

selvaduraiece commented 2 years ago

Here is how I implemented it. I am doing handoff as well along with this specific session transcript.

Startup.cs Make the below changes - I am using Cosmos DB.

            private CosmosDbPartitionedStorageOptions cosmosDbPartitionedStorage = new 
            CosmosDbPartitionedStorageOptions();

    public IStorage ConfigureStorage(IConfiguration Configuration)
    {
        IStorage storage;
        var settings = new BotSettings();
        Configuration.Bind(settings);

        cosmosDbPartitionedStorage.AuthKey = settings?.CosmosDb?.AuthKey;
        cosmosDbPartitionedStorage.CosmosDbEndpoint = settings?.CosmosDb?.CosmosDbEndpoint;
        cosmosDbPartitionedStorage.DatabaseId = settings?.CosmosDb?.DatabaseId;
        cosmosDbPartitionedStorage.ContainerId = settings?.CosmosDb?.ContainerId;

        storage = new CosmosDbPartitionedStorage(cosmosDbPartitionedStorage);

        return storage;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers().AddNewtonsoftJson();
        services.AddBotRuntime(Configuration);

        //Selva
        var storage = ConfigureStorage(Configuration);
        services.AddSingleton(storage);
        var userState = new UserState(storage);
        services.AddSingleton(userState);

        services.AddSingleton<IMiddleware>(s =>
                                   new BotComposer(
                                       userState,
                                   storage,
                                   cosmosDbPartitionedStorage));

    }

Create a folder middleware and inside create "BotComposer.cs" and add below code:

    using Microsoft.Azure.Cosmos;
    using Microsoft.Bot.Builder;
    using Microsoft.Bot.Builder.Azure;
    using Microsoft.Bot.Builder.Teams;
    using Microsoft.Bot.Schema;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Linq;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text.RegularExpressions;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Net.Mail;
    using Microsoft.Bot.Schema.Teams;

    namespace ********
    {

        public class TranscriptLog
    {
        public List<Conversation> conversations { get; set; } = new List<Conversation>();
    }

    public class QueryCollection
    {
        public List<QueryDetails> queryDetails { get; set; } = new List<QueryDetails>();
    }
    public class Conversation
    {
        public string UserMsg { get; set; } = null;
        public string BotMsg { get; set; } = null;
    } 
    public class Document
    {
        public string channel { get; set; } = null;
        public string conversationId { get; set; } = null;
        public string id { get; set; } = null;
        public string userName { get; set; } = null;
        public string userId { get; set; } = null;
        public string userEmail { get; set; } = null;
        public string LastUpdatedOn { get; set; } = null;
        public long prevTransaction { get; set; } = 0;
        public long currTransaction { get; set; } = 0;
        public TranscriptLog conversations { get; set; }
        public string userPrincipalName { get; set; } = null;
        public List<QueryDetails> queryDetails { get; set; }
        public QueryCollection queryCollection { get; set; }
        public object channelData { get; set; } = new object();

    }

    public class Transcript
    {
        public string id { get; set; }
        public string realId { get; set; }
        public Document document { get; set; }
        public string _etag { get; set; }
        public string PartitionKey { get; set; }
        public string _rid { get; set; }
        public string _self { get; set; }
        public string _attachments { get; set; }
        public int _ts { get; set; }
    }

  public class BotComposer : IMiddleware
  {
      private CosmosClient client;
      private readonly IStorage storage;
      private readonly CosmosDbPartitionedStorageOptions settings;
      private readonly string DatabaseId;
      private readonly string ContainerId;
      private readonly string EndPoint;
      private readonly string AuthKey;
      private readonly UserState userState;
      public BotComposer(UserState userState, IStorage storage, CosmosDbPartitionedStorageOptions cosmosDbPartitionedStorageOptions)
      {
          this.userState = userState;
          this.storage = storage;
          this.DatabaseId = cosmosDbPartitionedStorageOptions.DatabaseId;
          this.ContainerId = cosmosDbPartitionedStorageOptions.ContainerId;

          client = new CosmosClient(cosmosDbPartitionedStorageOptions.CosmosDbEndpoint, cosmosDbPartitionedStorageOptions.AuthKey, new CosmosClientOptions
          {
              ConnectionMode = ConnectionMode.Direct,
              IdleTcpConnectionTimeout = new TimeSpan(0, 10, 0)

          });
      }
      public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken))
{
    if (turnContext is null)
    {
        throw new ArgumentNullException(nameof(turnContext));
    }

    bool UpdateTransaction = false;
    Conversation conversation = new Conversation();
    turnContext.Activity.Locale = "en-US";

    if (turnContext?.Activity?.Type == "message" && turnContext?.Activity?.Type != ActivityTypes.Typing)
    {
        ///This is my version of code. Change as per your requirement!
        // conversation.UserMsg - To store User input
        // conversation.BotMsg - To store Bot response 

        conversation.UserMsg = null;
        conversation.UserMsg = turnContext?.Activity?.Text != null && turnContext?.Activity?.Text != string.Empty && turnContext?.Activity?.Text != "" ? "(" + DateTime.Now.ToString("yyyy'-'MM'-'dd HH:mm:ss") + "GMT) User : " + turnContext?.Activity?.Text : "(" + DateTime.Now.ToString("yyyy'-'MM'-'dd HH:mm:ss") + "GMT) User : null";
    }

    turnContext.OnSendActivities(async (sendContext, activities, nextSend) =>
    {
        conversation.BotMsg = null;
        // Log activities here
        foreach (var item in activities)
        {
            if (item.Type != ActivityTypes.Typing)
            {
                if (item.Type == ActivityTypes.Message && item.Text != null && item.Text != string.Empty && item.Type != ActivityTypes.Typing)
                {
                    conversation.BotMsg = conversation.BotMsg == null ? "(" + DateTime.Now.ToString("yyyy'-'MM'-'dd HH:mm:ss ") + "GMT) AskIT : " + item.Text + Environment.NewLine : conversation.BotMsg += item.Text + Environment.NewLine;
                }
                await AddLogs(turnContext, UpdateTransaction, conversation, cancellationToken).ConfigureAwait(true);
            }
        }
        return await nextSend();
    });
    await next(cancellationToken).ConfigureAwait(false);
}

public async Task AddLogs(ITurnContext turnContext, bool updateTransaction, Conversation conversation, CancellationToken cancellationToken)
{
    try
    {
        TranscriptLog logItems = new TranscriptLog();
        Document document = new Document();
        QueryCollection queryCollection = new QueryCollection();

        try
        {
            Database database = await client.CreateDatabaseIfNotExistsAsync(DatabaseId);
            Container container = await database.CreateContainerIfNotExistsAsync(ContainerId, "/id");
            string query = "SELECT * FROM c WHERE c.id = '" + Regex.Replace(turnContext?.Activity?.Conversation?.Id, @"\s+", "") + "'";
            QueryDefinition queryDefinition = new QueryDefinition(query);

            using (FeedIterator<Transcript> feedIterator = container.GetItemQueryIterator<Transcript>(queryDefinition))
            {
                while (feedIterator.HasMoreResults)
                {
                    FeedResponse<Transcript> response = (FeedResponse<Transcript>)await feedIterator.ReadNextAsync();
                    foreach (var item in response)
                    {
                        document = item.document;
                    }
                }
            }
            if (document is null)
            {
                logItems = null;
            }
            else
            {
                logItems = document.conversations;
                if (document.queryCollection != null)
                {
                    queryCollection = document.queryCollection;
                }
                else
                {
                    queryCollection = null;
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error Occured in Cosmos Block - Message: ", ex.Message);
            Console.WriteLine("Error Occured in Cosmos Block: - Stack trace: ", ex.StackTrace);
        }

        // If no stored messages were found, create and store a new entry.
        if (logItems is null)
        {
            // Add the current utterance to a new object.
            logItems = new TranscriptLog();
            logItems.conversations.Add(conversation);
            queryCollection = new QueryCollection();

            document = new Document();
            document.prevTransaction = 0;
            document.currTransaction = 0;
            document.conversationId = turnContext?.Activity?.Conversation?.Id;
            document.id = turnContext?.Activity?.Conversation?.Id;
            document.channel = turnContext?.Activity?.ChannelId;
            document.userId = turnContext?.Activity?.From?.Id;
            document.userName = turnContext.Activity?.From?.Name;
            document.conversationId = turnContext?.Activity?.Conversation?.Id;
            document.id = turnContext?.Activity?.Conversation?.Id;
            document.prevTransaction = 0;
            document.currTransaction = 0;

            if (turnContext.Activity.ChannelId == "msteams")
            {
                var member = new List<TeamsChannelAccount>();
                string continuationToken = null;
                do
                {
                    var currentPage = await TeamsInfo.GetPagedMembersAsync(turnContext, 2, continuationToken, cancellationToken);
                    continuationToken = currentPage.ContinuationToken;
                    member.AddRange(currentPage.Members);
                }

                while (continuationToken != null);
                List<string> info = new List<string>();
                document.userEmail = member[0].Email;
                document.userPrincipalName = member[0].UserPrincipalName;
            }
            else
            {
                document.userEmail = string.Empty;
                document.userPrincipalName = turnContext.Activity?.From?.Name;
            }

            document.channelData = turnContext?.Activity?.ChannelData;
            document.LastUpdatedOn = DateTime.Now.ToString("yyyy'-'MM'-'dd HH:mm:ss");
            document.queryCollection = queryCollection;
            document.conversations = logItems;
            var changes = new Dictionary<string, object>();
            {
                changes.Add(turnContext?.Activity?.Conversation?.Id, JObject.FromObject(document));
            }
            try
            {
                // Save the user message to your Storage.
                await storage.WriteAsync(changes, cancellationToken).ConfigureAwait(true);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error Occured in Cosmos Block - Message: ", ex.Message);
                Console.WriteLine("Error Occured in Cosmos Block: - Stack trace: ", ex.StackTrace);
            }
        }
        // Else, our storage already contained saved user messages, add new one to the list.
        else
        {
            document.channelData = turnContext?.Activity?.ChannelData;
            var count = logItems.conversations.Count;
            if ((count > 0 && (logItems.conversations[count - 1].BotMsg != conversation.BotMsg || logItems.conversations[count - 1].UserMsg != conversation.UserMsg)))

            {

                logItems.conversations.Add(conversation);
                document.queryCollection = queryCollection;
                document.LastUpdatedOn = DateTime.Now.ToString("yyyy'-'MM'-'dd HH:mm:ss");

                // Create Dictionary object to hold new list of messages.
                if (updateTransaction)
                {
                    document.prevTransaction = document.currTransaction;
                    document.currTransaction = document.conversations.conversations.Count - 1;
                }
                var changes = new Dictionary<string, object>();
                {
                    changes.Add(turnContext?.Activity?.Conversation?.Id, JObject.FromObject(document));
                }
                try
                {
                    // Save new list to your Storage.
                    await storage.WriteAsync(changes, cancellationToken).ConfigureAwait(true);
                    conversation.UserMsg = null;
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error Occured in Cosmos Block - Message: ", ex.Message);
                    Console.WriteLine("Error Occured in Cosmos Block: - Stack trace: ", ex.StackTrace);
                }
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Error Occured in Cosmos Block - Message: ", ex.Message);
        Console.WriteLine("Error Occured in Cosmos Block: - Stack trace: ", ex.StackTrace);
    }
}

} }

RaviKasaudhan commented 1 year ago

@selvaduraiece

Are you able to find a solution for this, I am also looking to extract the transcript alone.

Hey Aparnaji, Are you able to get the bot transcript now?