If you liked what you read feel free to connect with me on linkedin or follow me on dev.to :)
Hi and welcome to the third and possibly final part on my series on how to create a chatbot to perform sentiment analysis on tweets searched for.
In this post I'll go through how you can create a chatbot to interact with the function app that we developed in the pervious part. You can check out the repos for this project by clicking on the links below:
https://github.com/Albert-Bennett/TwitterSentimentAnalysisFunctionApp
https://github.com/Albert-Bennett/TwitterSentimentAnalysisChatBot
Setting up our project
We are starting simple enough by creating an echo bot using the template project to get these make sure to install them:
You can find a link to package here: https://marketplace.visualstudio.com/items?itemName=BotBuilder.botbuilderv4
With the template installed we can start by creating an echo bot. It is one of the most barebones templates for bots, all it does is repeat back your input.
To test the bot what you will need to do is to run the Bot Framework Emulator and run your chat bot project. With the chat bot up and running you should see something like this:
From here just copy the local host URL appended with /api/messages and paste into the bot framework emulator and you should see something like this when it has been successfully connected:
And that's our basic bot setup and testing so we know everything is working correctly at this point.
Connecting to our function app
To start we need to figure out what we need to send on to our app. There was nothing complex with it, everything that it needs to search can be sent in the query parameters. So our service needs to send a get request to a URL similar to this: http://localhost:7071/api/SentimentAnalysisFN?hashtags=[search terms]&max_tweets=[max number of tweets to find]
Super so with this the only method in our service should look like this:
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;
}
In case your wondering how I modeled the data for the response I copied it from the function app 😑. Of course I ran into an issue when deserializing the response from the function app. I thought serializing an enum in the response would return an int. Instead it returns a string.
The adaptive card
This part is a bit complex but, more from a structure and modeling point of view. To start off we need to decide how to present the data to the user. Here is a quick sketch of how I want the data to be shown:
I think it looks neat and with that we can start developing our solution.
Adaptive cards are constructed in a hierarchical structure meaning that we had a root component and add children to it of varying types like image cards and text input.
With that in mind here is the code to structure the adaptive card kinda like the mock image:
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;
}
Although there is a lot of code above it all just boils down to structuring the card in a way that I think presents that data in an nice way to the user. It's just a lot of... for each statements and ifs to process the response from the sentiment analysis. See snapshot below for the output in the bot:
The wiring up
The last part is just wiring up and sequencing the calls to the various services to make the bot... work. Here is how the main bot should look like.
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));
}
}
All it's doing is getting the users input from the chat bot and passing it on the service that handles the interaction with the function app and then passing that result on to the adaptive card creator to create the output for the bot. There is also a little error handling as well.
And that's it really. You should have a chat bot that you can use to do sentiment analysis on tweets that you search for. Of course there are some tweeks... that can be done, firstly because I was doing a lot of testing I had set the max number of tweets to search for to be 2 this can be increased to whatever you want. I'd suggest a much higher number like 10 or even 50. It's up to you the higher the number of searched tweets the more accurate the statistics at the end (although is only analyzing a snapshot of all tweets containing the search term so...). You can also increase the number of popular tweets to be returned as well, I think 2 - 5 is fine. There are issues you can run into with the adaptive cards if they get too big, as in they can get cut off or throw errors.
There are a number of improvements that can also be made across the system, things like asking the user for how many popular tweets to return and how many to search for could be a good idea as well as removing the limitation of only searching for tweets with a particular hash tag. I did it for flavour reasons and also it was the first thing that came to mind when I was thinking about a twitter search function. You can also go full on with the Azure services, by using things like an app service to host the function app and a bot service to host the bot and also adding app insights to implement some sort of logging.
Thanks for reading and I hope to see you guys next time.
Top comments (0)