In the previous post, we saw what is OpenAI function calling and how to use it to chat with your organization's user directory using Microsoft Graph. Please have a look at the article here: Chat with your user directory using OpenAI functions and Microsoft Graph
In this post, we will implement function calling for a very common scenario of augmenting the large language model's responses with data fetched from internet search.
Since the Large Language model (LLM) was trained with data only up to a certain date, we cannot talk to it about events which happened after that date. To solve this, we will use OpenAI function calling to call out the Bing Search API and then augment the LLM's responses with the data returned via internet search.
This pattern is called Retrieval Augmented Generation or RAG.
Let's look at the code now on how to achieve this. In this code sample I have used the following nuget packages:
https://www.nuget.org/packages/Azure.AI.OpenAI/1.0.0-beta.6/
https://www.nuget.org/packages/Azure.Identity/1.10.2/
The very first thing we will look at is our function definition for informing the model that it can call out to external search API to search information:
"functions": [ | |
{ | |
"name": "search_online", | |
"description": "Search online if the requested information cannot be found in the language model or the information could be present in a time after the language model was trained", | |
"parameters": { | |
"type": "object", | |
"required": [ | |
"searchQuery" | |
], | |
"properties": { | |
"searchQuery": { | |
"type": "string", | |
"description": "the text to search online to get the necessary information" | |
} | |
} | |
} | |
} | |
], |
static async Task Main(string[] args) | |
{ | |
var openaiApiKey = "<azure-openi-key>"; | |
var openaiEndpoint = "<azure-openi-endpoint>"; | |
var modelDeploymentName = "gpt-35-turbo"; //azure deployment name | |
var bingAPIKey = "<bing-api-key>"; | |
var credential = new AzureKeyCredential(openaiApiKey); | |
var openAIClient = new OpenAIClient(new Uri(openaiEndpoint), credential); | |
//get userQuestion from the console | |
Console.ForegroundColor = ConsoleColor.Yellow; | |
Console.WriteLine("Ask a question to the internet: "); | |
Console.ResetColor(); | |
string userQuestion = Console.ReadLine(); | |
//1. Call Open AI Chat API with the user's question. | |
Response<ChatCompletions> result = await CallChatGPT(userQuestion, modelDeploymentName, openAIClient); | |
//2. Check if the Chat API decided that for answering the question, a function call to Bing needs to be made. | |
var functionCall = result.Value.Choices[0].Message.FunctionCall; | |
if (functionCall != null) | |
{ | |
Console.ForegroundColor = ConsoleColor.Green; | |
Console.WriteLine($"Function Name: {functionCall.Name}, Params: {functionCall.Arguments}"); | |
Console.ResetColor(); | |
if (functionCall.Name == "search_online") | |
{ | |
//3. If the Bing function call needs to be made, the Chat API will also provide which parameters need to be passed to the function. | |
var searchOnlineParams = JsonSerializer.Deserialize<SearchOnlineParams>(functionCall.Arguments); | |
Console.WriteLine($"Search Query: {searchOnlineParams.searchQuery}"); | |
//4. Call Bing with the parameters provided by the Chat API | |
var searchResults = await SearchAsync(bingAPIKey, searchOnlineParams.searchQuery, 3); | |
//5. Call the Chat API again with the function response. | |
var functionMessages = new List<ChatMessage>(); | |
functionMessages.Add(new ChatMessage(ChatRole.Assistant, functionCall.Arguments) { Name = functionCall.Name }); | |
functionMessages.Add(new ChatMessage(ChatRole.Function, searchResults) { Name = functionCall.Name }); | |
result = await CallChatGPT(userQuestion, modelDeploymentName, openAIClient, functionMessages); | |
//6. Print the final response from the Chat API. | |
Console.WriteLine("------------------"); | |
//Write in yellow color | |
Console.ForegroundColor = ConsoleColor.Yellow; | |
Console.WriteLine(result.Value.Choices[0].Message.Content); | |
Console.ResetColor(); | |
} | |
} | |
else | |
{ | |
Console.WriteLine(result.Value.Choices[0].Message.Content); | |
} | |
} |
The Bing Web Search API key can be found in the "Keys and Endpoint" section on the Azure resource:
public static async Task<string> SearchAsync(string apiKey, string query, int count = 1, int offset = 0, CancellationToken cancellationToken = default) | |
{ | |
if (count <= 0) { throw new ArgumentOutOfRangeException(nameof(count)); } | |
if (count >= 50) { throw new ArgumentOutOfRangeException(nameof(count), $"{nameof(count)} value must be less than 50."); } | |
if (offset < 0) { throw new ArgumentOutOfRangeException(nameof(offset)); } | |
Uri uri = new($"https://api.bing.microsoft.com/v7.0/search?q={Uri.EscapeDataString(query)}&count={count}&offset={offset}"); | |
using HttpResponseMessage response = await SendGetRequest(apiKey, uri, cancellationToken).ConfigureAwait(false); | |
response.EnsureSuccessStatusCode(); | |
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); | |
BingSearchResponse? data = JsonSerializer.Deserialize<BingSearchResponse>(json); | |
WebPage[]? results = data?.WebPages?.Value; | |
string pagesContent = string.Empty; | |
foreach (var result in results) | |
{ | |
//To keep this simple for the sample code, we are only going to return the search results snippet. | |
//In production, ideally you will need to get the result.Url property of the search result and then get the content of the page using the Url. | |
pagesContent += result.Snippet; | |
} | |
return pagesContent; | |
} | |
//For production scenarious, please re-use httpclient as shown here: https://www.aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/ | |
private static async Task<HttpResponseMessage> SendGetRequest(string apiKey, Uri uri, CancellationToken cancellationToken = default) | |
{ | |
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri); | |
httpRequestMessage.Headers.Add("Ocp-Apim-Subscription-Key", apiKey); | |
return await new HttpClient().SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); | |
} |
private static async Task<Response<ChatCompletions>> CallChatGPT(string userQuestion, string modelDeploymentName, OpenAIClient openAIClient, IList<ChatMessage> functionMessages = null) | |
{ | |
var chatCompletionOptions = new ChatCompletionsOptions(); | |
chatCompletionOptions.Messages.Add(new ChatMessage(ChatRole.System, $"You are a helpful assistant. If you don't know the answer to a question, you can look it up online. Today's date in UTC is {DateTime.UtcNow}")); | |
chatCompletionOptions.Messages.Add(new ChatMessage(ChatRole.User, userQuestion)); | |
if (functionMessages != null) | |
{ | |
foreach (var functionMessage in functionMessages) | |
{ | |
chatCompletionOptions.Messages.Add(functionMessage); | |
} | |
} | |
chatCompletionOptions.Functions.Add(new FunctionDefinition() | |
{ | |
Name = "search_online", | |
Description = "Search online if the requested information cannot be found in the language model or the information could be present in a time after the language model was trained", | |
Parameters = BinaryData.FromString("{\"type\": \"object\",\"required\": [\"searchQuery\"],\"properties\": {\"searchQuery\": {\"type\": \"string\",\"description\": \"the text to search online to get the necessary information\"}}}\r\n") | |
}); | |
chatCompletionOptions.Temperature = 0; | |
var result = await openAIClient.GetChatCompletionsAsync(modelDeploymentName, chatCompletionOptions); | |
return result; | |
} |
This way, we can use Open AI function calling together with Bing Web Search API to connect our chat bot to the internet!