8/13/2023 Admin

Azure OpenAI RAG Pattern using Functions and a SQL Vector Database


image

This article will demonstrate how Azure OpenAI Function calling can be used with the Retrieval Augmented Generation (RAG) pattern.

The advantages of using this method over the method described in the article: Azure OpenAI RAG Pattern using a SQL Vector Database are:

  • This method allows the model to make multiple vector searches if needed
  • The developer is not required to create a complex  prompt of instructions

 

Rag Pattern VS Functions

image

As described in the article: Azure OpenAI RAG Pattern using a SQL Vector Database the RAG pattern is a technique for building natural language generation systems that can retrieve and use relevant information from external sources.

The concept is to first retrieve a set of passages that are related to the search query, then use them to supply grounding to the prompt, to finally generate a natural language response that incorporates the retrieved information.

image

Using Azure OpenAI Functions can achieve the same results, but rather than pushing grounding to the model, it can use Function calling to pull in the grounding information.

Function calling is a feature of the Azure OpenAI API that enables developers to describe functions to the model and have it intelligently return a JSON object containing arguments to call those functions.

The model can call these functions to perform actions such as searching and even updating a database, sending an email, or triggering a service to turn off a light bulb.

The model is also able to make multiple recursive function calls to perform multi-step processes and remains in control of the overall orchestration.

Function calling is covered more in depth in the article: Recursive Azure OpenAI Function Calling.

 

The Sample Application

image

We will start with the code from the article: Azure OpenAI RAG Pattern using a SQL Vector Database (you can download that code from the Downloads page on this site).

We will alter the code to use Functions.

 

Requirements

image

We need to use a version of the Azure.AI.OpenAI NuGet package that supports functions.

Upgrade the existing Nuget Package to beta6 (or higher).

 

image

As covered in the article: Recursive Azure OpenAI Function Calling ensure that you create a gpt- model deployment that is Model version 0613 (or higher).

Note: At the time of this writing, this is only possible if your Azure OpenAI service is created in the EAST Azure region.

Note: If you get this error: "Unrecognized request argument supplied: functions" you don't have a high enough version of the gpt- model deployed.

 

image

As covered in the article: Azure OpenAI RAG Pattern using a SQL Vector Database ensure you have deployed the text-embedding-ada-002 model to use for the embeddings.

 

Update The Existing Index Page

image

Open the AzureOpenAIChat project from the article: Azure OpenAI RAG Pattern using a SQL Vector Database in Visual Studio and open the Index.razor page.

Add the following using statements to the top of the file:

 

@using System.Text.Json;
@using System.Text.Json.Nodes;

 

Replace the entire existing @code section with the following code:

 

@code {
    string Endpoint = "";
    string DeploymentOrModelName = "";
    string Key = "";
    List<ChatMessage> ChatMessages = new List<ChatMessage>();
    string prompt = "";
    string ErrorMessage = "";
    bool Processing = false;
    string SystemMessage = "";
    // Declare an embedding collection and a list to store similarities
    List<ArticleResultsDTO> similarities = new List<ArticleResultsDTO>();
    protected override void OnInitialized()
    {
        // Get the Azure OpenAI Service configuration values
        Endpoint =
        _configuration["AzureOpenAIServiceOptions:Endpoint"] ?? "";
        DeploymentOrModelName =
        _configuration["AzureOpenAIServiceOptions:DeploymentOrModelName"] ?? "";
        Key =
        _configuration["AzureOpenAIServiceOptions:Key"] ?? "";
        // Add the system message
        SystemMessage = "You are helpful Assistant. ";
        SystemMessage += "You will always try to limit your answers to information ";
        SystemMessage += "obtained by calling a function to perform a vector search. ";
        SystemMessage += "You will always reply with a Markdown formatted response.";
        RestartChatGPT();
    }
    protected override async Task
    OnAfterRenderAsync(bool firstRender)
    {
        try
        {
            await _jsRuntime.InvokeAsync<string>(
                "ScrollToBottom", "chatcontainer"
            );
        }
        catch
        {
            // do nothing if this fails
        }
    }
    void RestartChatGPT()
    {
        ErrorMessage = "";
        prompt = "How can I bring my own data to Azure OpenAI?";
        // Create a new list of ChatPrompt objects and initialize it with the
        // system's introductory message
        ChatMessages = new List<ChatMessage>();
        ChatMessages.Add(
            new ChatMessage(
            ChatRole.System,
            SystemMessage)
        );
        StateHasChanged();
    }
}

 

This sets up the global fields and implements the OnInitialized() method that will run when the page first loads. This method retrieves the Azure OpenAI settings and inserts the system message, that will instruct the model on its expected behavior, into the ChatMessages collection.

It also implements the OnAfterRenderAsync method that will execute each time the page is refreshed. It calls the ScrollToBottom JavaScript method that will keep the chat window scrolled to the bottom of the chat.

Finally, it implements the RestartChatGPT method that will reset the chat.

 

Call Search Data

The CallSearchData() method is the primary method that is executed after the end user enters a chat prompt and presses the Call ChatGPT button.

Use the following code to implement the skeleton of the method:

 

  async Task CallSearchData()
  {
      // Global variables
      ChatCompletionsOptions chatCompletionsOptions = new ChatCompletionsOptions();
      Response<ChatCompletions> responseWithoutStream;
      List<ChatChoice> chatChoices = new List<ChatChoice>();
      ChatCompletions ChatCompletionsResult;
      ChatChoice chatChoice;
      // Set the in-progress flag to true
      Processing = true;
      // Notify the framework that the state has changed
      // and the UI should be updated
      StateHasChanged();
      try
      {
          // *** Call the Azure OpenAI Service ***
      }
      catch (Exception ex)
      {
          // Create an error notification message
          var Notification = new NotificationMessage()
              {
                  Severity = NotificationSeverity.Error,
                  Summary = "Error",
                  Detail = ex.Message,
                  Duration = 40000
              };
          // Show the notification
          NotificationService.Notify(Notification);
      }
      // Set the in-progress flag to false
      Processing = false;
      // Clear the prompt
      prompt = "";
      // Notify the framework that the state has changed
      // and the UI should be updated
      StateHasChanged();
  }

 

Inside the try catch block of the CallSearchData() method replace // *** Call the Azure OpenAI Service *** with the following code:

 

            // Clear any previous error messages
            ErrorMessage = "";
            // Clear similarities
            similarities = new List<ArticleResultsDTO>();
            // Remove old chat messages to avoid exceeding model limit
            RemoveOldChatMessags();
            // Create a new OpenAIClient object
            // with the provided API key and Endpoint
            OpenAIClient client = new OpenAIClient(
                new Uri(Endpoint),
                new AzureKeyCredential(Key));
            // Add the new message to chatMessages
            ChatMessages.Add(new ChatMessage(ChatRole.User, prompt));

 

 

This calls the RemoveOldChatMessags() method (to be implemented later) to will ensure there are not too many tokens passed to the model. Otherwise, this would produce an error.

This also retrieves the Azure OpenAI settings and instantiates an instance of the OpenAIClient that will be used to make calls to the Azure OpenAI API.

Finally, it inserts the prompt, that the end-user entered into the chat box, into the ChatMessages collection.

 

Define The Function

Next, add the following code:

 

            // *** Define the Function ***
            string fnVectorDatabaseSearchDescription =
            "Retrieves content from a vector database. ";
            fnVectorDatabaseSearchDescription +=
            "Use this function to retrieve content to answer any question. ";
            var fnVectorDatabaseSearch = new FunctionDefinition();
            fnVectorDatabaseSearch.Name = "VectorDatabaseSearch";
            fnVectorDatabaseSearch.Description = fnVectorDatabaseSearchDescription;
            fnVectorDatabaseSearch.Parameters = BinaryData.FromObjectAsJson(new JsonObject
                {
                    ["type"] = "object",
                    ["properties"] = new JsonObject
                    {
                        ["prompt"] = new JsonObject
                        {
                            ["type"] = "string",
                            ["description"] =
                                        "Provides content for any question asked by the user."
                        }
                    },
                    ["required"] = new JsonArray { "prompt" }
                });
            var DefinedFunction = new List<FunctionDefinition>
            {
                fnVectorDatabaseSearch
            };

 

This provides the definition of the function that the model can call.

Multiple functions could be defined at this point.

 

Call The Azure OpenAI Service

Now, add the following code:

 

            // *** Call Azure OpenAI Service ***
            chatCompletionsOptions = new ChatCompletionsOptions()
                {
                    Temperature = (float)0.7,
                    MaxTokens = 2000,
                    NucleusSamplingFactor = (float)0.95,
                    FrequencyPenalty = 0,
                    PresencePenalty = 0,
                };
            // Force the model to call the function
            chatCompletionsOptions.FunctionCall = fnVectorDatabaseSearch;
            chatCompletionsOptions.Functions = DefinedFunction;
            // Add the prompt to the chatCompletionsOptions object
            foreach (var message in ChatMessages)
            {
                chatCompletionsOptions.Messages.Add(message);
            }
            // Call the GetChatCompletionsAsync method
            responseWithoutStream =
            await client.GetChatCompletionsAsync(
                DeploymentOrModelName,
                chatCompletionsOptions);
            // Get the ChatCompletions object from the response
            ChatCompletionsResult = responseWithoutStream.Value;
            if (ChatCompletionsResult != null)
            {
                if (ChatCompletionsResult.Choices != null)
                {
                    chatChoices = ChatCompletionsResult.Choices.ToList();
                }
            }
            chatChoice = chatChoices.FirstOrDefault();

 

This creates a ChatCompletionsOptions object that contains the chat messages and the function definition.

This is passed to the instance of the OpenAIClient that was created earlier and the response is stored in the chatChoice object.

At this point we expect that the model will call the function (because we set chatCompletionsOptions.FunctionCall to the function definition).

Add the following code that will detect this and call the ExecuteFunction method (to be implemented later):

 

        if (chatChoice != null)
        {
            if (chatChoice.Message != null)
            {
                // Add it to the messages list
                ChatMessages.Add(chatChoice.Message);
                // See if as a response model wants to call a function
                if (chatChoice.Message.FunctionCall != null)
                {
                    // The model wants to call a function
                    // To allow the model to call multiple functions
                    // We need to start a While loop
                    bool FunctionCallingComplete = false;
                    while (!FunctionCallingComplete)
                    {
                        if (chatChoice != null)
                        {
                            // Call the function
                            ChatMessages = ExecuteFunction(chatChoice, ChatMessages);
                        }
                        // Get a response from the model
                        // (now that is has the results of the function)
                        // Create a new ChatCompletionsOptions object
                        chatCompletionsOptions = new ChatCompletionsOptions()
                            {
                                Temperature = (float)0.0,
                                MaxTokens = 2000,
                                NucleusSamplingFactor = (float)1.00,
                                FrequencyPenalty = 0,
                                PresencePenalty = 0,
                            };
                        // Use FunctionDefinition.Auto to give the model the option
                        // to call a function or not
                        chatCompletionsOptions.FunctionCall = FunctionDefinition.Auto;
                        chatCompletionsOptions.Functions = DefinedFunction;
                        // Remove old chat messages to avoid exceeding model limit
                        RemoveOldChatMessags();
                        // Add the prompt to the chatCompletionsOptions object
                        foreach (var message in ChatMessages)
                        {
                            chatCompletionsOptions.Messages.Add(message);
                        }
                        // Call the GetChatCompletionsAsync method
                        responseWithoutStream =
                        await client.GetChatCompletionsAsync(
                            DeploymentOrModelName,
                            chatCompletionsOptions);
                        // Get the ChatCompletions object from the response
                        ChatCompletionsResult = responseWithoutStream.Value;
                        if (ChatCompletionsResult != null)
                        {
                            if (ChatCompletionsResult.Choices != null)
                            {
                                chatChoices = ChatCompletionsResult.Choices.ToList();
                            }
                        }
                        chatChoice = chatChoices.FirstOrDefault();
                        if (chatChoice?.Message != null)
                        {
                            ChatMessages.Add(chatChoice.Message);
                            if (chatChoice?.Message?.FunctionCall != null)
                            {
                                // Keep looping
                                FunctionCallingComplete = false;
                            }
                            else
                            {
                                // Break out of the loop
                                FunctionCallingComplete = true;
                            }
                        }
                        else
                        {
                            // Break out of the loop
                            FunctionCallingComplete = true;
                        }
                    }
                }
            }
        }

 

Note that this time we set chatCompletionsOptions.FunctionCall to FunctionDefinition.Auto.

This means that the model can decide if it wants to call the function to perform additional vector searches (usually it won’t).

We do this in a while loop to allow the model to make multiple calls if needed.

 

Execute Function

Add the following code to implement the method that will respond to the request to execute the function:

 

    private List<ChatMessage> ExecuteFunction(
       ChatChoice chatChoice, List<ChatMessage> ParamChatPrompts)
    {
        // Get the arguments
        var functionArgs = chatChoice.Message.FunctionCall.Arguments;
        // Get the function name
        var functionName = chatChoice.Message.FunctionCall.Name;
        // Call the method that will perform a vector search
        // and populate the similarities collection
        // that holds the results of the search
        PerformVectorDatabaseSearch(functionArgs);
        // Deserialize the similarities collection
        // into functionResult that contains the matching articles
        string functionResult =
        JsonSerializer.Serialize<List<ArticleResultsDTO>>(similarities);
        // Return with the results of the function
        var ChatFunctionMessage = new ChatMessage();
        ChatFunctionMessage.Role = ChatRole.Function;
        ChatFunctionMessage.Content = functionResult;
        ChatFunctionMessage.Name = functionName;
        ParamChatPrompts.Add(ChatFunctionMessage);
        return ParamChatPrompts;
    }

 

Perform The Vector Search

 

The ExecuteFunction calls the PerformVectorDatabaseSearch method to perform a vector search of the database and build a collection of matching article chunks.

Add the following code to implement that method:

 

   void PerformVectorDatabaseSearch(string InputPrompt)
   {
       try
       {
           // Clear the list of similarities
           // That holds the results of the search
           similarities.Clear();
           // Create a new OpenAIClient object
           // with the provided API key and Endpoint
           OpenAIClient openAIClient = new OpenAIClient(
               new Uri(Endpoint),
               new AzureKeyCredential(Key));
           // Get embeddings for the search text
           var SearchEmbedding =
           openAIClient.GetEmbeddings(
               "text-embedding-ada-002",
               new EmbeddingsOptions(prompt)
           );
           // Get embeddings as an array of floats
           var EmbeddingVectors =
               SearchEmbedding.Value.Data[0].Embedding
               .Select(d => (float)d).ToArray();
           // Loop through the embeddings
           List<VectorData> AllVectors = new List<VectorData>();
           for (int i = 0; i < EmbeddingVectors.Length; i++)
           {
               var embeddingVector = new VectorData
                   {
                       VectorValue = EmbeddingVectors[i]
                   };
               AllVectors.Add(embeddingVector);
           }
           // Convert the floats to a single string to pass to the function
           var VectorsForSearchText =
           "[" + string.Join(",", AllVectors.Select(x => x.VectorValue)) + "]";
           // Call the SQL function to get the similar content articles
           var SimularContentArticles =
           @Service.GetSimilarContentArticles(VectorsForSearchText);
           // Loop through SimularContentArticles
           foreach (var Article in SimularContentArticles)
           {
               // Add to similarities collection
               similarities.Add(
                   new ArticleResultsDTO()
                       {
                           Article = Article.ArticleName,
                           Sequence = Article.ArticleSequence,
                           Contents = Article.ArticleContent,
                           Match = Article.cosine_distance ?? 0
                       }
                   );
           }
           // Sort the results by similarity in descending order
           similarities.Sort((a, b) => b.Match.CompareTo(a.Match));
           // Take the top 10 results
           similarities = similarities.Take(10).ToList();
           // Sort by the first column then the second column
           similarities.Sort((a, b) => a.Sequence.CompareTo(b.Sequence));
           similarities.Sort((a, b) => a.Article.CompareTo(b.Article));
       }
       catch (Exception ex)
       {
           // Create an error notification message
           var Notification = new NotificationMessage()
               {
                   Severity = NotificationSeverity.Error,
                   Summary = "Error",
                   Detail = ex.Message,
                   Duration = 40000
               };
           // Show the notification
           NotificationService.Notify(Notification);
           // Set the in-progress flag to false
           Processing = false;
           // Notify the framework that the state has changed
           // and the UI should be updated
           StateHasChanged();
       }
   }

 

Remove Old Chat Messages

Finally, implement the following method, called by earlier code, that will remove older chat messages to ensure we don’t pass too many tokens to the model:

 

    int CurrentWordCount = 0;
    private void RemoveOldChatMessags()
    {
        // Copy current chat messages to a new list
        var CopyOfChatMessages = new List<ChatMessage>(ChatMessages);
        // Clear the chat messages
        ChatMessages = new List<ChatMessage>();
        // Create a new LinkedList of ChatMessages
        LinkedList<ChatMessage> ChatPromptsLinkedList = new LinkedList<ChatMessage>();
        // Loop through the ChatMessages and add them to the LinkedList
        foreach (var item in CopyOfChatMessages)
        {
            ChatPromptsLinkedList.AddLast(item);
        }
        // Set the current word count to 0
        CurrentWordCount = 0;
        // Reverse the chat messages to start from the most recent messages
        foreach (var chat in ChatPromptsLinkedList.Reverse())
        {
            if (chat.Content != null)
            {
                int promptWordCount = chat.Content.Split(
                    new char[] { ' ', '\t', '\n', '\r' },
                    StringSplitOptions.RemoveEmptyEntries).Length;
                if (CurrentWordCount + promptWordCount >= 1000)
                {
                    // This message would cause the total to exceed 1000 words,
                    // so break out of the loop
                    break;
                }
                // Add to ChatMessages
                ChatMessages.Insert(0, chat);
                // Update the current word count
                CurrentWordCount += promptWordCount;
            }
        }
        // Check to see if the system message has been removed
        bool SystemMessageFound = false;
        foreach (var chat in ChatMessages)
        {
            if (chat.Role == ChatRole.System)
            {
                SystemMessageFound = true;
            }
        }
        // Add the system message if it was removed
        if (!SystemMessageFound)
        {
            ChatMessages.Add(
                new ChatMessage(
                ChatRole.System,
                SystemMessage)
            );
        }
    }

 

Links

Recursive Azure OpenAI Function Calling

What Is Azure OpenAI And Why Would You Want To Use It?

Bring Your Own Data to Azure OpenAI

Blazor and Azure OpenAI

Creating A Blazor Chat Application With Azure OpenAI

Azure OpenAI RAG Pattern using a SQL Vector Database

 

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.

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