5/27/2024 Admin

Using OpenAI to Update A Blazor Excel Worksheet


image

Watch the YouTube vide at this link: https://www.youtube.com/watch?v=sKNHlM6GdCc

 

What happens when you combine Microsoft Blazor with a new cutting-edge open source data editor and the latest Open AI 4o model?

In this blog post, we’ll explore how the OpenAI LLM model can be used to update an Excel-like data editor control within a Blazor application. We can use AI to update data based on rules it infers from data edits you make.

 

The Application

 

image

In this first example we edit the phone number field to add the area code.

 

image

When then click the Submit button.

 

image

The OpenAI LLM will analyze the data changed and deduce that the remaining phone numbers are missing their area code.

We ask the AI to send us the data grid cells that it determines need to be changed and we programmatically highlight them.

 

image

We can click the Log button to see the logic the AI used.

 

image

When can then click the Apply Changes button to instruct the AI to indicate what the cell values should be based on the rules it inferred.

Notice in this case the AI was able to determine what the correct area code is based on the value in the City column.

 

image

In the next example we demonstrate that you can make changes to multiple columns.

 

Creating The Code

image

When we look at the Visual Studio project we see that only a few files were added or updated:

 

  • Home – Contains the user interface for the sample
  • OpenAIServiceOptions – Simple class to hold the OpenAI key
  • OpenAIService – Handles calls to OpenAI
  • Appsettings – Configuration for the OpenAI key
  • Program – Registration of the BlazorDataSheet and OpenAI-DotNet packages

 

image

The first step is to install the required Nuget packages.

 

image

Install the following packages:

 

 

Open the Program.cs file and add the following code:

 

   builder.Services.AddBlazorDatasheet();
   builder.Services.AddScoped<OpenAIService>();

 

The BlazorDatasheet control also requires that you add the following to the App.razor page:

 

<link href="_content/BlazorDatasheet/sheet-styles.css" rel="stylesheet" />
<script src="_content/BlazorDatasheet/blazor-datasheet.js" type="text/javascript"></script>

 

image

You will also need to obtain an OpenAI key (See: https://platform.openai.com/account/api-keys) and put that key in the appsettings.json file.

 

The Excel-Like Data Editor

image

The sample uses the open-source BlazorDatasheet control that provides a Microsoft Excel-like editing experience in a Microsoft Blazor application.

You can see a demo of that control here:  https://anmcgrath.github.io/BlazorDatasheet/

 

image

The next step is to open the Home.razor control and add the control using the following code:

 

<Datasheet Sheet="@sheet" />

 

image

By itself this wont display anything so we will add a dropdown using this code:

 

<select @onchange="@(async (args) => await LoadTableData(args.Value?.ToString()))">
    <option value="TableOne">Table One</option>
    <option value="TableTwo">Table Two</option>
    </select>

 

This will allow us to select a set of sample data that will populate a DataTable. We will then use that DataTable to populate the BlazorDatasheet control using this code:

 

   private async Task LoadTableData(string paramTableName)
   {
       IsLoadingSheet = true;
       strAIResponse = "";
       Message = "";
       InitializeDataTable(paramTableName);
       // Get a list of the columns from the DataTable
       TableColumns = OrginalDataTable.Columns.Cast<DataColumn>().Select(x => x.ColumnName).ToList();
       // Create sheet with the number of columns
       sheet = new Sheet(OrginalDataTable.Rows.Count, TableColumns.Count);
       // Wait 1 second before loading the table data
       // to join the UI thread
       await Task.Delay(1000);
       // Turn off history
       sheet.BatchUpdates();
       sheet.Commands.PauseHistory();
       int i = 0;
       foreach (string objDatabaseColumn in TableColumns)
       {
           sheet.Columns.SetHeadings(i, i, objDatabaseColumn);  
           i++;
       }
       int ii = 0;
       foreach (DataRow dataRow in OrginalDataTable.Rows)
       {
           i = 0;
           foreach (string objDatabaseColumn in TableColumns)
           {
               // Set the value of the cell
               sheet.Cells[ii, i].Value = dataRow[i].ToString();
               // Make the first column read only and hidden and an integer
               if (i==0)
               {
                   sheet.Cells[ii, i].Format = new CellFormat() { IsReadOnly = true };
                   sheet.Cells[ii, i].Type = "int";
               }
               else
               {
                   sheet.Cells[ii, i].Format = new CellFormat() { IsReadOnly = false };
                   sheet.Cells[ii, i].Type = "string";
               }
               i++;
           }
           ii++;
       }
       // Hide the Id column
       sheet.Columns.SetWidth(0, 0);
       // Turn on history
       sheet.EndBatchUpdates();
       sheet.Commands.ResumeHistory();
       IsLoadingSheet = false;
   }

 

image

The data will display in the data editor. The data can now be updated.

We can then hit the Submit button to send the previous and updated data to the OpenAI 4o model.

The first step is to convert the data in the BlazorDatasheet back to a DataTable using the following code:

 

private async Task SubmitData()
{
    // Initialize a new DataTable
    CurrentDataTable = new DataTable();
    // Define columns based on TableColumns
    foreach (var column in TableColumns)
    {
        CurrentDataTable.Columns.Add(new DataColumn(column));
    }
    // Populate the DataTable with rows from the sheet
    for (int i = 0; i < sheet.NumRows; i++)
    {
        DataRow row = CurrentDataTable.NewRow();
        for (int j = 0; j < TableColumns.Count; j++)
        {
            try
            {
                string currentValue = sheet.Cells[i, j].Value?.ToString() ?? "";
                currentValue = currentValue.Replace("\r\n", " ")
                                           .Replace("\t", " ")
                                           .Replace("\r", " ")
                                           .Replace("\n", " ")
                                           .Trim();
                row[j] = currentValue;
            }
            catch
            {
                row[j] = "";
            }
        }
        CurrentDataTable.Rows.Add(row);
    }
    await GetChanges();
}

 

This calls the GetChanges method that passes the initial and updated DataTables to the AI with instructions to determine what has changed and to return JSON that indicates what data cells have changed:

 

            StringBuilder sb = new StringBuilder();
            sb.AppendLine("Please examine the following data to determine how the data has changed.");
            sb.AppendLine("");
            sb.AppendLine("For all data here are the column headers.");
            sb.AppendLine("");
            sb.AppendLine(string.Join(",", TableColumns));
            sb.AppendLine("");
            sb.AppendLine("The following show the #Original and #Updated Data:");
            sb.AppendLine("");
            var drOrginal = OrginalDataTable.Rows;
            var drCurrent = CurrentDataTable.Rows;
            // Convert to Arrays
            var arrOrginal = drOrginal.Cast<DataRow>().Select(x => x.ItemArray).ToArray();
            var arrCurrent = drCurrent.Cast<DataRow>().Select(x => x.ItemArray).ToArray();
            // Convert each item array to a string for proper joining
            var originalStrings = arrOrginal.Select(array => string.Join(",", array.Select(item => item.ToString())));
            var currentStrings = arrCurrent.Select(array => string.Join(",", array.Select(item => item.ToString())));
            sb.AppendLine("#Original: " + string.Join("; ", originalStrings));
            sb.AppendLine("#Updated: " + string.Join("; ", currentStrings));  
            sb.AppendLine("");
            sb.AppendLine("Infer any validation rules such as spelling or formatting or other logical rules. ");
            sb.AppendLine("Infer that any changed data is the correct data and the previous data is incorrect. ");
            sb.AppendLine("");
            sb.AppendLine("");
            sb.AppendLine("Please indicate what cells in #Updated, that have not been updated, ");
            sb.AppendLine("that should be highlighted as being affected by the validation rules. ");
            sb.AppendLine("As the final response please return a .json result surounded by ###JSON-START and JSON-END###. ");
            sb.AppendLine("Use this format: [{ 'row number': 'value', 'column number': 'value', 'validation Reason': 'reason1'}]");
            sb.AppendLine("For example: ");
            sb.AppendLine("{'row number': '1', 'column number': '2', 'validation Reason': 'Data missing'},");
            sb.AppendLine("{'row number': '2', 'column number': '4', 'validation Reason': 'Invalid format'}");
            sb.AppendLine("");
            // *** Call AI
            // Create a list of messages
            var messages = new List<OpenAI.Chat.Message>
            {
                new OpenAI.Chat.Message(Role.System, "You are a helpful assistant."),
                new OpenAI.Chat.Message(Role.User, sb.ToString()),
            };
            // Call the OpenAI service
            ChatMessages = await OpenAIService.CallOpenAIService(messages);
            // Get the last response
            strAIResponse = ChatMessages.LastOrDefault();
            // Get the JSON response from the content between ###JSON-START and JSON-END###
            int intStart = strAIResponse.IndexOf("###JSON-START###");
            int intEnd = strAIResponse.IndexOf("###JSON-END###");
            // Get the JSON from the response
            string strJSON = "";
            if (intStart > 0 && intEnd > 0)
            {
                strJSON = strAIResponse.Substring(intStart + 16, intEnd - intStart - 16);
            }    

 

 

That JSON is passed though the AI again to ensure that it is valid:

 

 public async Task<string> CallOpenAIServiceToGetJSON(string? jsonText)
 {
     // Get the API key from the appsettings.json file
     var ApiKey = _openAIServiceOptions.ApiKey;
     // Create a new instance of OpenAIClient using
     // the ApiKey and Organization
     var api =
     new OpenAIClient(new OpenAIAuthentication(ApiKey));
     var messages = new List<OpenAI.Chat.Message>
     {
         new OpenAI.Chat.Message(
             Role.System, 
             $"Please correct this json to make it valid. Return only the valid json in a collection called data: {jsonText}"),
     };
     var chatRequest = new OpenAI.Chat.ChatRequest(
         messages, temperature: 0.1, responseFormat: ChatResponseFormat.Json, model: "gpt-4o");
     var ChatResponse =
     await api.ChatEndpoint.GetCompletionAsync(chatRequest);
     return ChatResponse.FirstChoice.Message;
 }

 

The JSON is then used to highlight the fields using the following code:

 

                // Parse the JSON
                ValidationDatas json = JsonConvert.DeserializeObject<ValidationDatas>(strJSON);
                // If there is data, then highlight the cells
                if (json.Data.Count > 0)
                {
                    sheet.BatchUpdates();
                    // Loop through the JSON and highlight the cells
                    foreach (var item in json.Data)
                    {
                        int row = Convert.ToInt32(item.RowNumber);
                        int column = Convert.ToInt32(item.ColumnNumber);
                        sheet.Cells[(row - 1), (column - 1)].Format = new CellFormat() 
                        { BackgroundColor = "yellow", ForegroundColor = "black", FontWeight = "bold" };
                    }
                    sheet.EndBatchUpdates();
                    CanApplyChanges = true;
                }
                else
                {
                    Message = "No JSON data was returned.";
                    CanApplyChanges = false;
                }

 

 

image

Once the cells are highlighted, we can click the Apply Changes to request that the AI determine what the values should be changed to based on the rules it has now inferred.

The following code is used:

 

                StringBuilder sb = new StringBuilder();
                sb.AppendLine("");
                sb.AppendLine("Infer any validation rules such as spelling or formatting or other logical rules. ");
                sb.AppendLine("Infer that any changed data is the correct data and the previous data is incorrect. ");
                sb.AppendLine("");
                sb.AppendLine("");
                sb.AppendLine("Please indicate what cells in #Updated, that have not been updated, ");
                sb.AppendLine("should have their values updated based on the validation rules. ");
                sb.AppendLine("As the final response please return a .json result surounded by ###JSON-START and JSON-END###. ");
                sb.AppendLine("Use this format: [{ 'row number': 'value', 'column number': 'value', 'updated value': 'value'}]");
                sb.AppendLine("For example: ");
                sb.AppendLine("{'row number': '1', 'column number': '2', 'updated value': '5'},");
                sb.AppendLine("{'row number': '2', 'column number': '4', 'updated value': '(213) 555-1212'}");
                sb.AppendLine("");
                // *** Call AI
                ChatMessages.Add(new OpenAI.Chat.Message(Role.User, sb.ToString()));
                // Call the OpenAI service
                ChatMessages = await OpenAIService.CallOpenAIService(ChatMessages);
                // Get the last response
                strAIResponse = ChatMessages.LastOrDefault();
                // Get the JSON response from the content between ###JSON-START and JSON-END###
                int intStart = strAIResponse.IndexOf("###JSON-START###");
                int intEnd = strAIResponse.IndexOf("###JSON-END###");
                // Get the JSON from the response
                string strJSON = "";
                if (intStart > 0 && intEnd > 0)
                {
                    strJSON = strAIResponse.Substring(intStart + 16, intEnd - intStart - 16);
                }

 

 

The resulting JSON is used to update the BlazorDatasheet using the following code:

 

                    // If there is data...
                    if (json.Data.Count > 0)
                    {
                        sheet.BatchUpdates();
                        // Loop through the JSON and update the cells
                        foreach (var item in json.Data)
                        {
                            try
                            {       
                                int row = Convert.ToInt32(item.RowNumber);
                                int column = Convert.ToInt32(item.ColumnNumber);
                                string value = item.UpdatedValue;
                                sheet.Cells[(row - 1), (column - 1)].Clear();
                                sheet.Cells[(row - 1), (column - 1)].Value = value;
                                sheet.Cells[(row - 1), (column - 1)].Format = new CellFormat() 
                                { BackgroundColor = "lightgreen", ForegroundColor = "black", FontWeight = "bold" };
                            }
                            catch { }
                        }
                        sheet.EndBatchUpdates();
                        CanApplyChanges = true;
                    }
                    else
                    {
                        Message = "No JSON data was returned.";
                        CanApplyChanges = false;
                    }

 

image

Download

The project is available on the Downloads page on this site.

You must have Visual Studio 2022 (or higher) installed to run the code.

 

Links

https://github.com/anmcgrath/BlazorDatasheet

https://github.com/RageAgainstThePixel/OpenAI-DotNet

An error has occurred. This application may no longer respond until reloaded. Reload 🗙