As announced in this post, Azure Open AI now supports Function Calling feature.
I don't explain what it is, but I share my experiment result and C# code.
Prerequisites
- Azure Subscription and Open AI account
- Deploy model that supports Function Calling, e.g. gpt-35-turbo-16k
Test scenario
I wonder if LLM can chain functions if needed. How it behaves when it has multiple functions, etc. So, I tested.
Firstly, I added two functions.
GetWeatherFunction.cs
public class GetWeatherFunction
{
static public string Name = "get_current_weather";
// Return the function metadata
static public FunctionDefinition GetFunctionDefinition()
{
return new FunctionDefinition()
{
Name = Name,
Description = "Get the current weather in a given location",
Parameters = BinaryData.FromObjectAsJson(
new
{
Type = "object",
Properties = new
{
Location = new
{
Type = "string",
Description = "The city and state, e.g. San Francisco, CA",
},
Unit = new
{
Type = "string",
Enum = new[] { "Celsius", "Fahrenheit" },
}
},
Required = new[] { "location" },
},
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }),
};
}
// The function implementation. It always returns 31 for now.
static public Weather GetWeather(string location, string unit)
{
return new Weather() { Temperature = 31, Unit = unit };
}
}
// Argument for the function
public class WeatherInput
{
public string Location { get; set; } = string.Empty;
public string Unit { get; set; } = "Celsius";
}
// Return type
public class Weather
{
public int Temperature { get; set; }
public string Unit { get; set; } = "Celsius";
}
GetCapitalFunction.cs
public class GetCapitalFunction
{
static public string Name = "get_capital";
// Return the function metadata
static public FunctionDefinition GetFunctionDefinition()
{
return new FunctionDefinition()
{
Name = Name,
Description = "Get the capital of the location",
Parameters = BinaryData.FromObjectAsJson(
new
{
Type = "object",
Properties = new
{
Location = new
{
Type = "string",
Description = "The city, state or country, e.g. San Francisco, CA",
}
},
Required = new[] { "location" },
},
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }),
};
}
// The function implementation. It always return Tokyo for now.
static public string GetCapital(string location)
{
return "Tokyo";
}
}
// Argument for the function
public class CapitalInput
{
public string Location { get; set; } = string.Empty;
}
Then I register them in Program.cs
Uri openAIUri = new("https://<your account>.openai.azure.com/");
string openAIApiKey = "<your key>";
string model = "gpt-35-turbo-16k";
// Instantiate OpenAIClient for Azure Open AI.
OpenAIClient client = new(openAIUri, new AzureKeyCredential(openAIApiKey));
ChatCompletionsOptions chatCompletionsOptions = new();
ChatCompletions response;
ChatChoice responseChoice;
// Add function definitions
FunctionDefinition getWeatherFuntionDefinition = GetWeatherFunction.GetFunctionDefinition();
FunctionDefinition getCapitalFuntionDefinition = GetCapitalFunction.GetFunctionDefinition();
chatCompletionsOptions.Functions.Add(getWeatherFuntionDefinition);
chatCompletionsOptions.Functions.Add(getCapitalFuntionDefinition);
I set user question like below.
string question = "What's the weather in the capital city of Japan?";
chatCompletionsOptions.Messages.Add(new(ChatRole.User, question));
Then I call the Completion in a loop to see the finish reason is function or stop.
- If the finish reason is function call, then
- Get arguments value
- Call the function
- Register responses and results to
chatCompletionsOptions.Messages
. - Call LLM again with the history.
while (responseChoice.FinishReason == CompletionsFinishReason.FunctionCall)
{
// Add message as a history.
chatCompletionsOptions.Messages.Add(responseChoice.Message);
if (responseChoice.Message.FunctionCall.Name == GetWeatherFunction.Name)
{
string unvalidatedArguments = responseChoice.Message.FunctionCall.Arguments;
WeatherInput input = JsonSerializer.Deserialize<WeatherInput>(unvalidatedArguments,
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!;
var functionResultData = GetWeatherFunction.GetWeather(input.Location, input.Unit);
var functionResponseMessage = new ChatMessage(
ChatRole.Function,
JsonSerializer.Serialize(
functionResultData,
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
functionResponseMessage.Name = GetWeatherFunction.Name;
chatCompletionsOptions.Messages.Add(functionResponseMessage);
}
else if (responseChoice.Message.FunctionCall.Name == GetCapitalFunction.Name)
{
string unvalidatedArguments = responseChoice.Message.FunctionCall.Arguments;
CapitalInput input = JsonSerializer.Deserialize<CapitalInput>(unvalidatedArguments,
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!;
var functionResultData = GetCapitalFunction.GetCapital(input.Location);
var functionResponseMessage = new ChatMessage(
ChatRole.Function,
JsonSerializer.Serialize(
functionResultData,
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
functionResponseMessage.Name = GetCapitalFunction.Name;
chatCompletionsOptions.Messages.Add(functionResponseMessage);
}
// Call LLM again to generate the response.
response =
await client.GetChatCompletionsAsync(
model,
chatCompletionsOptions);
responseChoice = response.Choices[0];
}
Console.WriteLine(responseChoice.Message.Content);
Result
- I sent
"What's the weather in the capital city of Japan?"
to LLM. - LLM returns
CompletionsFinishReason.FunctionCall
to useget_capital
. - Send back the message to LLM again with function result (Tokyo).
- LLM returns
CompletionsFinishReason.FunctionCall
to useget_current_weather
. - Send back the message to LLM again with function result (31 degree.)
- LLM returns final response saying:
"The current weather in the capital city of Japan, Tokyo, is 31 degrees Celsius."
.
Conclusion
I see that LLM can chain functions depending on the user input. This behavior is similar with Semantic Kernel Planner, so I will compare them when I have time.
Top comments (1)
Did I miss to initialize responseChoice anywhere?