Если вам понравилось прочитанное, присоединяйтесь ко мне на 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 для реализации некоторого вида протоколирования.
Спасибо за прочтение и надеюсь увидеть вас в следующий раз.