Анализ настроения твитов в чат-боте — часть 3 Чат-бот

Если вам понравилось прочитанное, присоединяйтесь ко мне на linkedin или следите за мной на dev.to 🙂

Привет и добро пожаловать в третью и, возможно, заключительную часть моей серии статей о том, как создать чат-бота для проведения анализа настроений в твитах, которые ищут.

В этом посте я расскажу, как создать чат-бота для взаимодействия с функциональным приложением, которое мы разработали в предыдущей части. Вы можете ознакомиться с репозиториями для этого проекта, перейдя по ссылкам ниже:
https://github.com/Albert-Bennett/TwitterSentimentAnalysisFunctionApp
https://github.com/Albert-Bennett/TwitterSentimentAnalysisChatBot

Настройка нашего проекта
Мы начнем достаточно просто, создав бота echo, используя шаблонный проект, чтобы получить их, убедитесь, что установили их:

Вы можете найти ссылку на пакет здесь: https://marketplace.visualstudio.com/items?itemName=BotBuilder.botbuilderv4
Установив шаблон, мы можем начать с создания эхо-бота. Это один из самых простых шаблонов для ботов, все, что он делает, это повторяет ваш ввод.
Для тестирования бота вам нужно запустить Bot Framework Emulator и запустить ваш проект чат-бота. Если чат-бот запущен и работает, вы должны увидеть что-то вроде этого:

Отсюда просто скопируйте URL локального хоста с добавлением /api/messages и вставьте в эмулятор Bot Framework, после успешного подключения вы должны увидеть что-то вроде этого:

И это наша базовая настройка и тестирование бота, так что мы знаем, что все работает правильно на данный момент.

Подключение к нашему функциональному приложению
Для начала нам нужно выяснить, что нам нужно отправить в наше приложение. В этом нет ничего сложного, все, что ему нужно для поиска, можно отправить в параметрах запроса. Итак, наш сервис должен отправить запрос get на URL, подобный этому: http://localhost:7071/api/SentimentAnalysisFN?hashtags=[условия поиска]&max_tweets=[максимальное количество твитов для поиска].
Супер, поэтому единственный метод в нашем сервисе должен выглядеть следующим образом:

        public async Task<TwitterSentimentResponse> GetSentimentAnalysisForTweets(string searchTerm, int? maxResults = 10)
        {
            string url = $"{baseUrl}?hashtags={searchTerm}&max_tweets={maxResults}";

            using (var client = _httpClientFactory.CreateClient())
            {
                var response = await client.GetAsync(url);

                if (response.IsSuccessStatusCode)
                {
                    using var contentStream = await response.Content.ReadAsStreamAsync();

                    return await JsonSerializer.DeserializeAsync<TwitterSentimentResponse>(contentStream);
                }
            }

            return null;
        }
Войти в полноэкранный режим Выйти из полноэкранного режима

Если вам интересно, как я смоделировал данные для ответа, я скопировал их из функционального приложения 😑. Конечно, я столкнулся с проблемой при десериализации ответа из функционального приложения. Я думал, что сериализация перечисления в ответе вернет int. Вместо этого он возвращает строку.

Адаптивная карта
Эта часть немного сложна, но больше с точки зрения структуры и моделирования. Для начала нам нужно решить, как представить данные пользователю. Вот быстрый набросок того, как я хочу отобразить данные:

Я думаю, что это выглядит аккуратно, и с этим мы можем начать разработку нашего решения.
Адаптивные карточки построены по иерархической структуре, что означает, что у нас есть корневой компонент и к нему добавляются дочерние компоненты различных типов, например, карточки с изображениями и текстовым вводом.
В связи с этим ниже приведен код для создания адаптивной карточки, подобно тому, как это показано на макете:

        public static Attachment GetAnalysisCard(TwitterSentimentResponse data)
        {
            var cardBodyElements = new List<AdaptiveElement>
            {
                GetStatisticsElement(data.tweetSentimentAnalysis, data.numberOfTweetsFound)
            };

            cardBodyElements.AddRange(GetPopularTweetCards(data.mostPopularTweets));

            AdaptiveSchemaVersion defaultSchema = new(1, 0);

            AdaptiveCard card = new(defaultSchema)
            {
                Body = cardBodyElements
            };

            return CreateAdaptiveCardAttachment(card.ToJson());
        }

        static IEnumerable<AdaptiveElement> GetPopularTweetCards(TweetData[] mostPopularTweets)
        {
            List<AdaptiveElement> result = new List<AdaptiveElement>();

            foreach (TweetData tweet in mostPopularTweets)
            {
                result.AddRange(GetPopularTweetCard(tweet));
            }

            return result;
        }

        private static IEnumerable<AdaptiveElement> GetPopularTweetCard(TweetData tweet)
        {
            return new List<AdaptiveElement>
            {
                new AdaptiveTextBlock
                {
                        Size = AdaptiveTextSize.Medium,
                        Weight = AdaptiveTextWeight.Bolder,
                        Text = tweet.text,
                        Wrap = true
                },
                new AdaptiveColumnSet
                {
                    Columns = new List<AdaptiveColumn>
                    {
                        new AdaptiveColumn
                        {
                            Items = new List<AdaptiveElement>
                            {
                                new AdaptiveTextBlock
                                {
                                    Spacing = AdaptiveSpacing.None,
                                    IsSubtle = true,
                                    Wrap = true,
                                    Text = $"Likes: {tweet.public_metrics.like_count}"
                                },
                                new AdaptiveTextBlock
                                {
                                    Spacing = AdaptiveSpacing.None,
                                    IsSubtle = true,
                                    Wrap = true,
                                    Text = $"Retweets: {tweet.public_metrics.retweet_count}"
                                }
                            }
                        },
                        new AdaptiveColumn
                        {
                            Items = new List<AdaptiveElement>
                            {
                                new AdaptiveTextBlock
                                {
                                    Spacing = AdaptiveSpacing.None,
                                    IsSubtle = true,
                                    Wrap = true,
                                    Text = $"Replies: {tweet.public_metrics.reply_count}"
                                },
                                new AdaptiveTextBlock
                                {
                                    Spacing = AdaptiveSpacing.None,
                                    IsSubtle = true,
                                    Wrap = true,
                                    Text = $"Quote count: {tweet.public_metrics.quote_count}"
                                }
                            }
                        }
                    }
                }
            };
        }

        static AdaptiveColumnSet GetStatisticsElement(Dictionary<string, int> tweetSentimentAnalysis, int numberOfTweetsFound)
        {
            return new AdaptiveColumnSet
            {
                Columns = new List<AdaptiveColumn>
                {
                    new AdaptiveColumn
                    {
                        Items = GetCardStatistics(tweetSentimentAnalysis, numberOfTweetsFound),
                        Width = "stretch"
                    },
                    new AdaptiveColumn
                    {
                        Items = new List<AdaptiveElement>
                        {
                            new AdaptiveTextBlock
                            {
                                Spacing = AdaptiveSpacing.None,
                                IsSubtle = true,
                                Wrap = true,
                                Text = $"Number of Tweets found: {numberOfTweetsFound}"
                            }
                        }
                    }
                }
            };
        }

        static List<AdaptiveElement> GetCardStatistics(Dictionary<string, int> data, int numberOfTweetsFound)
        {
            List<AdaptiveElement> result = new List<AdaptiveElement>();

            foreach(string sentiment in data.Keys)
            {
                result.Add(new AdaptiveTextBlock
                {
                    Spacing = AdaptiveSpacing.None,
                    IsSubtle = true,
                    Wrap = true,
                    Text = $"{sentiment}: {(data[sentiment] * 100) / numberOfTweetsFound }%"
                });
            }

            return result; 
        }

        static Attachment CreateAdaptiveCardAttachment(string jsonData)
        {
            var adaptiveCardAttachment = new Attachment()
            {
                ContentType = "application/vnd.microsoft.card.adaptive",
                Content = JsonConvert.DeserializeObject(jsonData),
            };

            return adaptiveCardAttachment;
        }
Вход в полноэкранный режим Выход из полноэкранного режима

Хотя выше приведено много кода, все это сводится к структурированию карточки таким образом, чтобы, на мой взгляд, представить эти данные в удобном для пользователя виде. Это просто много… для каждого утверждения и ifs для обработки ответа от анализа настроения. Смотрите снимок ниже для вывода данных в боте:

Подключение
Последняя часть — это подключение и последовательность вызовов различных служб, чтобы бот… работал. Вот как должен выглядеть основной бот.

        protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
        {
            var userInput = turnContext.Activity.Text;
            var response = await _twitterAnalysisAppService.GetSentimentAnalysisForTweets(userInput, 2);

            if (response == null)
            {
                string errorText = "There was an issue contacting the function app";
                await turnContext.SendActivityAsync(MessageFactory.Text(errorText, errorText), cancellationToken);
            }
            else
            {
                var adaptiveCard = AdaptiveCardConstructor.GetAnalysisCard(response);
                await turnContext.SendActivityAsync(MessageFactory.Attachment(adaptiveCard));
            }
        }
Вход в полноэкранный режим Выход из полноэкранного режима

Все, что он делает, — это получает данные от пользователя через чат-бота, передает их сервису, который обрабатывает взаимодействие с функциональным приложением, а затем передает результат создателю адаптивной карты для создания выходных данных для бота. Есть также небольшая обработка ошибок.

Вот, собственно, и все. У вас должен быть чат-бот, который вы можете использовать для анализа настроений в твитах, которые вы ищете. Конечно, есть некоторые изменения… которые можно сделать, во-первых, поскольку я проводил много тестов, я установил максимальное количество твитов для поиска равным 2, это число можно увеличить до любого желаемого. Я бы предложил гораздо большее число, например 10 или даже 50. Это зависит от вас, чем больше число искомых твитов, тем точнее статистика в конце (хотя анализируется только снимок всех твитов, содержащих поисковый запрос, так что…). Вы также можете увеличить количество популярных твитов, которые будут возвращены, я думаю, что 2 — 5 — это нормально. Есть проблемы с адаптивными картами, если они становятся слишком большими, например, они могут обрезаться или выдавать ошибки.
Есть ряд улучшений, которые можно сделать в системе, например, спросить пользователя о том, сколько популярных твитов нужно вернуть и сколько их искать, а также убрать ограничение на поиск твитов только с определенным хэш-тегом. Я сделал это из соображений вкуса, а также это было первое, что пришло мне в голову, когда я думал о функции поиска в Твиттере. Вы также можете полностью использовать службы Azure, используя такие вещи, как служба приложений для размещения функционального приложения и служба ботов для размещения бота, а также добавляя app insights для реализации некоторого вида протоколирования.

Спасибо за прочтение и надеюсь увидеть вас в следующий раз.

Оцените статью
devanswers.ru
Добавить комментарий