-
Notifications
You must be signed in to change notification settings - Fork 0
Lab 6.2 ‐ Adding AI Semantic Kernel
By now, you have seen how we can use LLM's to help us convert existing software into new modern software that can even run in the cloud. But with the same models we can also augment our applications and make them "smarter".
With SSmart Components we got already some nice results, what if we could even make the application smarter and let it retrieve information from our systems and then fill out the fields using the internal data?
We are going to make again a change to the AddReservation
screen and we are going to change the smart paste capability on the form.
The way we do this, is by using a C# library called Semantic Kernel
, that makes it very easy to incorporate the features we used during our previous labs into your application.
With Semantic Kernel
, we are going to parse the email text, provide a system prompt with instructions to get data that we can bind to our form.
You add Semantic Kernel simply by adding the following NuGet Packages to the project:
<PackageReference Include="Microsoft.SemanticKernel" Version="1.25.0" />
Note: Currently the version of Semantic Kernel used in smart components is a different version as we are using in this lab. To ensure the two versions don't create problems, remove the package references to smart components that we added in the previous lab.
We are going to use the following system prompt:
You are an AI assistant that takes customer emails and converts them to a JSON structure that contains the datapoints you find in the email. The JSON structure looks as follows:
{
"checkInDate": "Check in Date in dd-MM-yyyy format",
"checkOutDate": "Check out date in dd-MM-yyyy format",
"FoodPackage": "Requested Food Package Id",
"RoomType": Requested Room Type,
"Name": "Customer full name",
"Address": "Customer address",
"PhoneNumber": "Customer phone number, preferred cell phone if available",
"Total": "total cost of the booking in USD",
"AdvancePayment": "Advance payment made at time of booking in USD",
"Remaining": "Remaining payment to be done at checkin in USD"
}
The user prompt will contain an email conversation that we can get from the clipboard, when we click a button that we call "smart paste".
The result should be a json string where there are no values for the properties FoodPackage
and RoomType
In order for the AI to know how to select a Food Package or a Room Type, we need to provide it this information. We can put it into the prompt, but we can also use the concept of plugins, where semeantic kernel will call functions for data it does not know how to retrieve.
In order to select the correct food package, we need to first query the database what food packages are available and then have the LLM decide which food package fits best for this customer request. To create such a function, you can create a C# function that will be called by Semantic Kernel automatically. We start by creating a class with a primary constructor, that will get the required parameters injected using dependency injection. The class looks as follows:
public class HotelPlugin(DataAccess.DataAccess Da, IConfiguration configuration)
{
private DataAccess.DataAccess Da { get; } = Da;
private IConfiguration configuration { get; } = configuration;
}
Next we are adding our plugins that can get the data from the database to provide the correct values for the food package and the room type.
We start with the Foodpackage selection. For this we create a fuction that will select the data from the database that contains all available food packages and we are using the LLM model to make a selection out of that list, based on wha tit knows about the customer from the email conversation pasted from the clipboard.
The function looks as follows:
[KernelFunction,
Description("returns the food package ID, based on the food preferences of the customer")]
[return: Description("The food package ID")]
public async Task<int> SelectFoodPreference([Description("The food preference of the customer")] string preferedFood)
{
string fdsql = "select FId, FName from FoodPackage;";
DataSet ds2 = Da.ExecuteQuery(fdsql);
string availablePackages = "";
foreach (DataRow row in ds2.Tables[0].Rows)
{
availablePackages += $"Fid: {row["Fid"]} , Food Package: {row["FName"]}\n";
}
//now we have available food packages in the form:
//Fid:11, Food Package:Rice+Chicken
//Fid:12, Food Package:Rice+Beef
//Fid:13, Food Package:Rice+Shrimp
// we use this to let the LLM decide which package matches best for this customer, given their food preference.
string systemPrompt =
$"""
You are tasked with selecting a food package based on the customers food preference. You must make a choice and you only return the Fid number as an integer that matches the package you selected. The food packages available are:
{availablePackages}
Respond with only the number as an integer value, no markdown or other markup!
The next user prompt will contain the customers food preference.
""";
ChatMessageContent result = await GetResultFromLLM(preferedFood, systemPrompt);
int parsedResult = int.Parse(result.Content);
return parsedResult;
}
We can do the same for selecting the correct room type, based on what we know from the customer. For this we can create a plugin that takes the details and from there determines what the best hotel room would be for them.
This function works more or less the same: it retrieves room type information from the database and it takes the input with details about the customer. Based on this it returns the best match room ID that is available.
[KernelFunction,
Description("returns the room ID of an available room based on the details we know about the customer")]
[return: Description("The room ID")]
public async Task<int> SelectRoomPreference([Description("The details about what we know about the customer that would influence the selection of a room type")] string customerdetails)
{
string roomsql = "select rID, Category from Room where IsBooked='No'";
DataSet ds2 = Da.ExecuteQuery(roomsql);
string availableRooms="";
foreach (DataRow row in ds2.Tables[0].Rows)
{
availableRooms += $"Room ID: {row["rID"]} , Room type: {row["Category"]}\n";
}
//now we have available room types in the form:
//Room ID:11, Room Type:Single
//Room ID:14, Room Type:Double King
//Room ID:16, Room Type:Double
// we use this to let the LLM decide which package matches best for this customer, given their food preference.
string systemPrompt =
$"""
You are tasked with selecting a room based on the information we have about the customer.
You must make a choice and you only return the Room ID number as an integer that matches
the room you selected. The rooms available are:
{availableRooms}
Respond with only the number as an integer value, no markdown or other markup!
The next user prompt will contain the details about the customer.
""";
ChatMessageContent result = await GetResultFromLLM(availableRooms, systemPrompt);
int parsedResult = int.Parse(result.Content);
return parsedResult;
}
Now both functions make use of a method called GetResultFromLLM and we pass it in the details where the model can make its selection and return the selected option. This function will set up a chat history that includes a system prompt and the user prompt.
This function looks as follows:
private async Task<ChatMessageContent> GetResultFromLLM(string userPrompt, string systemPrompt)
{
var history = new ChatHistory
{
new(AuthorRole.System,systemPrompt),
new(AuthorRole.User,userPrompt)
};
string deploymentName = configuration.GetSection("OpenAI").GetValue<string>("Model");
string endpoint = configuration.GetSection("OpenAI").GetValue<string>("EndPoint");
string apiKey = configuration.GetSection("OpenAI").GetValue<string>("ApiKey");
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(deploymentName, endpoint, apiKey)
.Build();
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var settings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions };
var result = await chatCompletionService.GetChatMessageContentAsync(history, settings, kernel);
return result;
}
Now that we have the functions available to get the data from the database, next we need to use Semantic Kernel to retrieve the data from an email thread the hotel has had with the customer.
We want to call a method that gets the data from the clipboard and then uses semantic kernel to get the data back so we can bind this in the UI.
We create a new class for this that takes care of all the AI interaction and setting up the Semantic Kernel in such a way that it will invoke the functions we just created. the class contains the system prompt that we will use in the function call to map the data.
Create a new class that looks as follows:
public static class Reservations
{
static string systemPrompt =
"""
You are an AI assitant that takes customer emails and converts it to a json string that contains the datapoints you find in the email. The json structure looks as follows:
{
"checkInDate": "Check in Date in dd-MM-yyyy format",
"checkOutDate": "Check out date in dd-MM-yyyy format",
"FoodPackage": "Requested Food Package Id, must be a number. if not found use 0",
"RoomType": Requested Room Type, must be a number when not found use 0,
"Name": "Customer full name",
"Address": "Customer address",
"PhoneNumber": "Customer phone number, prefered cell phone if available",
"Total": "total cost of the booking in USD",
"AdvancePayment": "Advance payment made at time of booking in USD",
"Remaining": "Remaining payment to be done at checkin in USD"
}
You only return the json string, no markdown or other markup!
""";
}
In the prompt you see that we instruct the LLM to retieve the data from any conversation that you migth have had with a customer. We also instruct it to set a value to a default when it can not find the data. We ask it to return a json string, since that is easy to convert to a class that contains the data and we can return this back to the caller. This data can then be used to databind to fields.
Now we add a function that we call GetFieldMappingFromClipboard
that takes in the clipboard text and the configuration system that we get from dependency injection. we need th configuration system so we can retrieve the values for the Model
, the EndPoint
and the ApiKey
The function looks as follows:
public static async Task<PasteResult> GetFieldMappingFromClipboard(string clipboardText, IConfiguration configuration)
{
string deploymentName = configuration.GetSection("OpenAI").GetValue<string>("Model");
string endpoint = configuration.GetSection("OpenAI").GetValue<string>("EndPoint");
string apiKey = configuration.GetSection("OpenAI").GetValue<string>("ApiKey");
var builder = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(deploymentName,
endpoint,
apiKey);
builder.Services.AddSingleton<IConfiguration>(configuration);
builder.Services.AddScoped<DataAccess.DataAccess>();
builder.Plugins.AddFromType<HotelPlugin>();
var kernel = builder.Build();
var history = new ChatHistory
{
new(AuthorRole.System,systemPrompt),
new(AuthorRole.User,clipboardText)
};
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var settings = new OpenAIPromptExecutionSettings {
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions ,
Temperature = 0.1,
TopP = 0.1,
MaxTokens = 4096
};
var result = await chatCompletionService.GetChatMessageContentAsync(history, settings, kernel);
// ensure the json string does not contain any markdown, since it is sometimes ignored in the respose.
var resultingJson = result.Content.Replace("json", "");
resultingJson = resultingJson.Replace("```", "");
var aiPasteResults = JsonConvert.DeserializeObject<PasteResult>(resultingJson);
return aiPasteResults;
}
You see in the configuration of the kernel, we pass in the same settings as we used in the AI playground.
Now, the thing with these models is that they not always return the same data and although we instructed to return just the json string, it still sometimes adds the markdown characters to the returned string. To remove this, you see calls to string.Replace
, so we ensure the string is stripped from any unwanted markdown, when we try to map it to a class.
The class we map the data to looks as follows:
public class PasteResult
{
public string CheckInDate { get; set; }
public string CheckOutDate { get; set; }
public int FoodPackage { get; set; }
public int RoomType { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string PhoneNumber { get; set; }
public string Total { get; set; }
public string AdvancePayment { get; set; }
public string Remaining { get; set; }
}
To handle the click event on the "smart paste" button to paste the clipboard email conversation, we then call the previously created function.
private async Task btnPasteOnclick()
{
clipboardText = await clipboard.ReadTextAsync();
var result = await Reservations.GetFieldMappingFromClipboard(clipboardText, Configuration);
dtpCheckIn = result.CheckInDate;
dtpCheckOut = result.CheckOutDate;
txtFId = result.FoodPackage.ToString();
txtRId = result.RoomType.ToString();
txtCName = result.Name;
txtAdd = result.Address;
txtPhone = result.PhoneNumber;
txtTotal = result.Total.ToString();
txtAdv = result.AdvancePayment.ToString();
txtRemaining = result.Remaining.ToString();
}
This method is bound to the button, using the razor syntax in the blazor page. The button definition looks as follows:
<button @onclick="btnPasteOnclick">Paste From Clipboard</button>
The only thing we need to also resolve is access to the Clipboard. For this we are using a library that abstacts this for us. Add the following package to the project file:
<PackageReference Include="CurrieTechnologies.Razor.Clipboard" Version="1.6.0" />
And in the App.razor
file we need to add an javascript library that creates the integration with the clipboard for us.
Add the following lines to the App.razor
file in your project:
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
<script src="_content/CurrieTechnologies.Razor.Clipboard/clipboard.min.js"></script>
</body>
Now we want to test if we can get a booking inserted with a simple smart paste option. For this, use the email conversation from the previous lab to see if it works. Select the text and put it on the clipboard. Now click the button Smart Paste that you created and see if the fields are filled out in the form according to your expectations.
You can expect that a room of type King
is selected, since it has a larger bed, and that the food option for a vegetarian is selected. That the check in date is Saturday 26th and check out date 28th. The telephone number should contain the cell phone number that is in the signature of Marcel.
You can test various types of rooms or food packages, by adding or removing rooms from the database using the blazor UI or making changes to the email conversation.