This article is part of #ServerlessSeptember. You'll find other helpful articles, detailed tutorials, and videos in this all-things-Serverless content collection. New articles from community members and cloud advocates are published every week from Monday to Thursday through September.
Find out more about how Microsoft Azure enables your Serverless functions at https://docs.microsoft.com/azure/azure-functions/.
Story behind my use-case
If you are developing any product, feedback is much more important. You have to trace each and every feedback like feature request or Bugs/Issues. Even Microsoft has a dedicated website https://feedback.azure.com/ for tracking all its feedback related to their Azure Product. For issues they have to use GitHub issues https://github.com/MicrosoftDocs/azure-docs/issues for all their open Source Projects. This would be a great way to track all the feedback on the open-source project. But how we can track Internal Project feedback efficiently!
Because ...
In this blog, we are going to use a better way to solve this problem of getting feedback about our internal projects with the help of azure server-less computing and AI š
Let's jump into our Project
Architecture
Let's assume that we are going to develop a new internal product called myproduct (wow what a unique name š¤£ )
I'm going to use Yammer to get all the feedback's from our internal users.
Yammer is your social layer across Microsoft 365, integrating with the apps and services you already use to stay productive. You can create and edit documents, take notes, and share resources as a group. And get back to your conversations from anywhere in Microsoft 365.
If you notice the architecture, most of the main components are server-less like azure functions and logic app. There are many advantages of using server-less is our architecture. some of them are:
- Cost Efficient (Consumption model)
- Less Development time (logic-app : drag/drop model)
- Quick Deployments etc.,
Components used
The components in this architecture are more flexible. i.e, you can easily replace one component with the other similar one.
For e.g
- You can use MS Teams or any other internal chat-based tool instead of yammer for getting the feedback.
- Azure DevOps WorkItem Tracking can be replaced by github/JIRA kind of tools easily.
- Cosmos DB can be replaced by the Azure Table Storage.
See how flexible this architecture is when we start using more and more server-less components š
Workflow
Here I'm focusing on a product that was developed for internal purposes. So our end-users are nothing but the employees in our organization. So I used Yammer as my main platform to gather all the feedback and bugs about myproduct from users.
Logic App
As soon as a user posted in yammer our logic app will be triggered. Let's see how our logic app now.
Yammer
LUIS
I'm using LUIS to predict what are the intents in the yammer post(utterances)
Intents: Bugsš /Features
Utterances: Yammer post which users posted
An Utterances are input from the user that your app needs to interpret.
An Intent represents a task or action the user wants to perform. It is a purpose or goal expressed in a user's utterance.
I trained by LUIS with some user-defined Intents. You can also find my exported LUIS model in the Github
AzureDevOps WorkItems
If the predicted intent is Bug then I'll create a bug work-item in AzureDevOps else if it is Feedback then I'll create Feature work-item as simple as that.
CosmosDB
All are fine, but what happens if the intent is neither bug nor feedback?
In such a case, None Intent will be our predicted Intent. This None Intent is more important for Re-training our LUIS model. So I'm going to save these intents safely in CosmosDB
Azure Functions
OK, I've saved the None Intents in cosmos DB, now I'm going to build a static website where developers can view all the information about the none intents in near real-time without doing any refresh.
This is quite an interesting part, where I've learned how powerful the Azure-Functions are! Thanks to Anthony Chu for his wonderful blog
Image Credits: Anthony Chu
I've used Signal-R for a Broadcasting real-time updates from cosmos DB using CosmosDB Change-Feed in Azure Function.
If you see the below image I've four azure functions totally.
Let's start with OnDocumentsChanged
class, which will be triggered automatically for every insert/update on the target Cosmos DB.
For every new/updated item in the cosmos DB, I'll simply adding those items as messages in the SignalRHub(SignalRHubItems)
public static class OnDocumentsChanged
{
[FunctionName("OnDocumentsChanged")]
public static async Task Run([CosmosDBTrigger(
databaseName: "MyProductDB",
collectionName: "Items",
ConnectionStringSetting = "AzureWebJobsCosmosDBConnectionString",
LeaseCollectionName = "leases",
CreateLeaseCollectionIfNotExists = true)]
IEnumerable<object> updatedItems,
[SignalR(HubName = "SignalRHubItems")] IAsyncCollector<SignalRMessage> signalRMessages,
ILogger log)
{
foreach(var item in updatedItems)
{
await signalRMessages.AddAsync(new SignalRMessage
{
Target = "SignalRHubUpdatedItems",
Arguments = new[] { item }
});
}
}
The SignalRInfo
class will be called from the static web page to make connections to SignalRhub to read the message from that.
public static class SignalRInfo
{
[FunctionName("SignalRInfo")]
public static IActionResult Run(
[HttpTrigger(AuthorizationLevel.Anonymous,"get","post")] HttpRequest req,
[SignalRConnectionInfo(HubName = "SignalRHubItems")] SignalRConnectionInfo connectionInfo,
ILogger log)
{
return new OkObjectResult(connectionInfo);
}
}
The GetItems
class will also be called from the static web page to make connections to CosmosDB to get/read all the Items.
public static class GetItems
{
[FunctionName("GetItems")]
public static IActionResult Run(
[HttpTrigger(AuthorizationLevel.Anonymous,"get","post")] HttpRequest req,
[CosmosDB("MyProductDB", "Items", ConnectionStringSetting = "AzureWebJobsCosmosDBConnectionString")]
IEnumerable<object> updatedItems,
ILogger log)
{
try{
return new OkObjectResult(updatedItems);
}
catch(Exception e){
log.LogError(e.Message.ToString());
}
return new OkObjectResult(updatedItems);
}
}
Finally StaticFileFunction
class is simple Az-function to show our statics web page (index.html), inside www
folder
public static class StaticFileFunction
{
const string staticFilesFolder = "www";
static string defaultPage = String.IsNullOrEmpty(GetEnvironmentVariable("DEFAULT_PAGE")) ?
"index.html" : GetEnvironmentVariable("DEFAULT_PAGE");
[FunctionName("StaticFileFunction")]
public static HttpResponseMessage Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
try
{
var filePath = GetFilePath(req, log);
var response = new HttpResponseMessage(HttpStatusCode.OK);
var stream = new FileStream(filePath, FileMode.Open);
response.Content = new StreamContent(stream);
response.Content.Headers.ContentType =new MediaTypeHeaderValue(GetMimeType(filePath));
return response;
}
catch (Exception e)
{
string name = e.Message.ToString();
return new HttpResponseMessage(HttpStatusCode.NotFound);
}
}
private static string GetScriptPath()
=> Path.Combine(GetEnvironmentVariable("HOME"), @"site\wwwroot");
private static string GetEnvironmentVariable(string name)
=> System.Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process);
private static string GetFilePath(HttpRequest req, ILogger log)
{
var path = req.Query["file"];
var staticFilesPath = Path.GetFullPath(Path.Combine(GetScriptPath(), staticFilesFolder));
var fullPath = Path.GetFullPath(Path.Combine(staticFilesPath, path));
if (!IsInDirectory(staticFilesPath, fullPath))
{
throw new ArgumentException("Invalid path");
}
var isDirectory = Directory.Exists(fullPath);
if (isDirectory)
{
fullPath = Path.Combine(fullPath, defaultPage);
}
return fullPath;
}
private static bool IsInDirectory(string parentPath, string childPath)
{
var parent = new DirectoryInfo(parentPath);
var child = new DirectoryInfo(childPath);
var dir = child;
do
{
if (dir.FullName == parent.FullName)
{
return true;
}
dir = dir.Parent;
} while (dir != null);
return false;
}
private static string GetMimeType(string filePath)
{
var provider = new FileExtensionContentTypeProvider();
string contentType;
provider.TryGetContentType(filePath, out contentType);
return contentType;
}
}
That's it! Let's test it out
Testing
Posted some posts in Yammer.
Checking AzureDevOps
It's created bugs and features correctly. However one of the posts got missed meaning it's predicted as None Intent.
Checking CosmosDB
So our post got inserted in cosmos DB correctly.
Static Web Page
Here is the live-stream
And it's Working Finally!
Source Code
This is an Open-Source Project, view the full source code in the below link and feel free to provide feedback/issues to me š
Serverless Prediction of a Product Feedback (#AzureDevStories)
This Project is created as a part of Azure Dev Stories Challenge and won the First Prize š
I've published a detailed article about this project in the Dev Community
Architecture Diagram
Components Used
- Yammer
- LUIS
- Logic Apps
- AzureDevOps WorkItems
- Cosmos DB
- Signal R
- Azure Functions
Yammer
Users provide their feedback about the product. It could be many, for the demo purpose I just choose 2 topics (Bug, Feature)
LUIS
Creating Intents for Bugs and Feedbacks in the LUIS.
Logic Apps
Predicting the Intents i.e, Bug or Feedback based on the Yammer Post by the user and take necessary actions
AzureDevOps WorkItems
Create Bug/Feature if the top intent of the Post matched with LUIS
Cosmos DB
Insert the document in Cosmos DB if the top intent of the Post is None
Signal R
Serverless Signal R used to autorefresh the WebPage for theā¦
Top comments (1)
This is fantastic! I've got to try this with logic apps, thanks for sharing.