Summary: This post explores OpenAI’s function calling capability and how to implement it in .NET applications. Learn how to define functions, process structured outputs, and build more reliable AI-powered features that integrate seamlessly with your existing codebase.
Introduction
In June 2023, OpenAI introduced a powerful new capability to their API: function calling. This feature represents a significant advancement in how developers can interact with large language models (LLMs), enabling more structured and reliable outputs that integrate seamlessly with application code.
Function calling allows developers to describe functions to the model, which can then intelligently decide when to call these functions based on user input. The model generates JSON that adheres to the function signature, making it straightforward to parse and execute the appropriate actions in your application.
For .NET developers, this capability opens up exciting possibilities for building more robust AI-powered applications. In this post, we’ll explore how to implement OpenAI function calling in .NET applications, covering everything from basic implementation to advanced patterns and best practices.
Understanding Function Calling
Before diving into implementation, let’s understand what function calling is and why it’s so valuable.
What is Function Calling?
Function calling is a capability that allows you to describe functions to the model, and the model will intelligently choose to output a JSON object containing arguments to call those functions. This creates a structured way for the model to return specific types of information or trigger specific actions in your application.
For example, instead of parsing free-form text to extract weather information, you can define a getWeather function that takes parameters like location and unit. When a user asks about the weather, the model will recognize this intent and return a structured JSON object with the appropriate parameters.
Why is Function Calling Valuable?
Function calling solves several key challenges in AI application development:
- Structured outputs: Get consistent, parseable responses instead of free-form text
- Reduced hallucinations: The model is constrained to return valid parameters for your functions
- Seamless integration: Connect AI capabilities directly to your application’s existing functions
- Improved user experience: Handle complex user requests more reliably
- Simplified development: Reduce the need for complex prompt engineering and output parsing
Setting Up Your .NET Environment
Let’s start by setting up a .NET project with the necessary packages to work with the Azure OpenAI Service or OpenAI API.
Creating a New Project
bash
dotnet new console -n OpenAiFunctionCalling
cd OpenAiFunctionCalling
Adding Required Packages
For Azure OpenAI Service:
bash
dotnet add package Azure.AI.OpenAI
For OpenAI API:
bash
dotnet add package OpenAI
Basic Function Calling Implementation
Let’s implement a basic example of function calling using the Azure.AI.OpenAI package.
Defining Functions
First, we need to define the functions that we want the model to be able to call:
csharp
using Azure;
using Azure.AI.OpenAI;
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
// Initialize the OpenAI client
string endpoint = "https://your-resource-name.openai.azure.com/";
string key = "your-api-key";
string deploymentName = "gpt-4"; // or gpt-35-turbo
OpenAIClient client = new OpenAIClient(new Uri(endpoint ), new AzureKeyCredential(key));
// Define the functions
var functions = new List<FunctionDefinition>
{
new FunctionDefinition
{
Name = "get_weather",
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" },
description = "The temperature unit to use"
}
},
required = new[] { "location" }
},
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
}
};
// Create chat completion options
var chatCompletionsOptions = new ChatCompletionsOptions
{
DeploymentName = deploymentName,
Messages =
{
new ChatMessage(ChatRole.System, "You are a helpful assistant."),
new ChatMessage(ChatRole.User, "What's the weather like in Boston?")
},
Functions = functions,
Temperature = 0.7f,
MaxTokens = 800
};
// Get the response
var response = await client.GetChatCompletionsAsync(chatCompletionsOptions);
var responseMessage = response.Value.Choices[0].Message;
// Check if the model wants to call a function
if (responseMessage.FunctionCall != null)
{
Console.WriteLine($"Function to call: {responseMessage.FunctionCall.Name}");
Console.WriteLine($"Arguments: {responseMessage.FunctionCall.Arguments}");
// Parse the arguments
var arguments = JsonDocument.Parse(responseMessage.FunctionCall.Arguments).RootElement;
string location = arguments.GetProperty("location").GetString();
string unit = arguments.TryGetProperty("unit", out var unitProperty)
? unitProperty.GetString()
: "celsius";
Console.WriteLine($"Parsed location: {location}");
Console.WriteLine($"Parsed unit: {unit}");
// Here you would call your actual weather service
string weatherResult = GetWeather(location, unit);
// Add the function result to the conversation
chatCompletionsOptions.Messages.Add(new ChatMessage(ChatRole.Function, weatherResult, responseMessage.FunctionCall.Name));
chatCompletionsOptions.Messages.Add(new ChatMessage(ChatRole.Assistant, responseMessage.Content, functionCall: responseMessage.FunctionCall));
// Get the final response
var finalResponse = await client.GetChatCompletionsAsync(chatCompletionsOptions);
Console.WriteLine($"Final response: {finalResponse.Value.Choices[0].Message.Content}");
}
else
{
Console.WriteLine($"Response: {responseMessage.Content}");
}
}
static string GetWeather(string location, string unit)
{
// In a real application, you would call a weather API here
// This is a mock implementation
return JsonSerializer.Serialize(new
{
location,
temperature = unit == "celsius" ? 22 : 72,
unit,
condition = "sunny"
});
}
}
Understanding the Code
Let’s break down what’s happening in this code:
- We initialize the Azure OpenAI client with our endpoint and API key.
- We define a
get_weatherfunction with parameters forlocationandunit. - We create a chat completion request with a user message asking about the weather.
- We check if the model’s response includes a function call.
- If it does, we parse the arguments and call our weather function.
- We add the function result back to the conversation and get a final response.
Running the Example
When you run this code with a query like “What’s the weather like in Boston?”, the model will recognize that it should call the get_weather function with “Boston” as the location. It will return a structured JSON object that you can parse and use to call your actual weather service.
Advanced Function Calling Patterns
Now that we understand the basics, let’s explore some more advanced patterns for using function calling in .NET applications.
Multiple Functions
In real applications, you’ll likely have multiple functions that the model can choose from. Let’s expand our example:
csharp
var functions = new List<FunctionDefinition>
{
new FunctionDefinition
{
Name = "get_weather",
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" },
description = "The temperature unit to use"
}
},
required = new[] { "location" }
},
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
},
new FunctionDefinition
{
Name = "get_restaurant_recommendations",
Description = "Get restaurant recommendations for a location",
Parameters = BinaryData.FromObjectAsJson(
new
{
type = "object",
properties = new
{
location = new
{
type = "string",
description = "The city and state, e.g. San Francisco, CA"
},
cuisine = new
{
type = "string",
description = "Type of cuisine, e.g. Italian, Chinese, etc."
},
price_range = new
{
type = "string",
@enum = new[] { "cheap", "moderate", "expensive" },
description = "Price range for the restaurants"
}
},
required = new[] { "location" }
},
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
}
};
With multiple functions defined, the model will intelligently choose which one to call based on the user’s input. For example, if the user asks “What are some good Italian restaurants in New York?”, the model will call the get_restaurant_recommendations function with the appropriate parameters.
Function Dispatching
When working with multiple functions, you’ll need a way to dispatch the function calls to the appropriate handlers. Here’s a pattern for doing this:
csharp
async Task<string> DispatchFunctionCall(string functionName, JsonElement arguments)
{
switch (functionName)
{
case "get_weather":
string location = arguments.GetProperty("location").GetString();
string unit = arguments.TryGetProperty("unit", out var unitProperty)
? unitProperty.GetString()
: "celsius";
return GetWeather(location, unit);
case "get_restaurant_recommendations":
string recLocation = arguments.GetProperty("location").GetString();
string cuisine = arguments.TryGetProperty("cuisine", out var cuisineProperty)
? cuisineProperty.GetString()
: null;
string priceRange = arguments.TryGetProperty("price_range", out var priceProperty)
? priceProperty.GetString()
: "moderate";
return GetRestaurantRecommendations(recLocation, cuisine, priceRange);
default:
throw new ArgumentException($"Unknown function: {functionName}");
}
}
string GetRestaurantRecommendations(string location, string cuisine, string priceRange)
{
// In a real application, you would call a restaurant API here
// This is a mock implementation
return JsonSerializer.Serialize(new
{
location,
cuisine,
priceRange,
restaurants = new[]
{
new { name = "Restaurant A", rating = 4.5 },
new { name = "Restaurant B", rating = 4.2 },
new { name = "Restaurant C", rating = 4.8 }
}
});
}
Handling Complex Parameter Types
Function calling supports complex parameter types, including nested objects and arrays. Here’s an example of a function with more complex parameters:
csharp
new FunctionDefinition
{
Name = "create_itinerary",
Description = "Create a travel itinerary based on user preferences",
Parameters = BinaryData.FromObjectAsJson(
new
{
type = "object",
properties = new
{
destination = new
{
type = "string",
description = "The travel destination"
},
start_date = new
{
type = "string",
description = "The start date of the trip (YYYY-MM-DD)"
},
end_date = new
{
type = "string",
description = "The end date of the trip (YYYY-MM-DD)"
},
preferences = new
{
type = "object",
properties = new
{
activities = new
{
type = "array",
items = new
{
type = "string",
@enum = new[] { "sightseeing", "museums", "food", "shopping", "nature", "relaxation" }
},
description = "Preferred activities during the trip"
},
accommodation_type = new
{
type = "string",
@enum = new[] { "hotel", "hostel", "apartment", "resort" },
description = "Preferred type of accommodation"
},
budget = new
{
type = "string",
@enum = new[] { "budget", "moderate", "luxury" },
description = "Budget level for the trip"
}
}
}
},
required = new[] { "destination", "start_date", "end_date" }
},
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
}