Function Calling: Extracting function parameters from natural language
Most applications interact with a backend API that is specifically structured by the company developing the app. To fit our app to this API, we build form-like input fields to get structured data that we can send to the API. However, now, using the power of LLMs and function calling, we can build a way for users to interact with our app in an unstructured way through natural language.
Let’s say we are building a restaurant booking app. We will have input fields for the restaurant name, date and time of booking, and the number of people. Using the CoreLocation framework (opens in a new tab), we can get the user’s location to figure out which restaurant with the specified name is closest to them for the reservation.
Once the user fills out these fields in the UI, we will pass the information to a function similar to the one below that will pass the information to our own API, make the booking, and return a true or false to confirm the booking:
func bookRestaurant(
_ name: String,
date: Date,
time: DateComponents,
numberOfPeople: Int
) async -> Bool {
// note: we can get the user's location via the CoreLocation framework
// Make API call to your company's backend to make the booking to a restaurant with the specified name closest to the user's location
// recieve back true / false response from the API to confirm the reservation
return reservationConfirmed
}
Now, instead of filling out the tedious form fields, the user is on the go, and they will just tell your app in natural language:
"Book a table for four at The Italian Place for tomorrow evening at 7 PM.”
We can use the power of LLMs to convert this natural language user input into a JSON object that we can then easily use inside our very structured bookRestaurant
function. To do this, we have to provide information about our bookRestaurant
function, specifically its name, description of what it does, and the arguments that it takes specified in the JSON format.
NOTE: _While function calling never directly calls the function in your app, it does provide the inputs for your function to use so that it can be called. _
Using Preternatural in combination with the CorePersistence (opens in a new tab) framework that comes with the AI framework, the first step is to specify the JSON object of bookRestaurant
function parameters that you would want to get returned from the LLM’s API based on the user’s natural language input:
struct RestaurantBookingParameters: Codable, Hashable, Sendable {
let name: String?
let date: String?
let time: String?
let numberOfPeople: Int?
var formattedDate: Date? {
let dateFormatter = DateFormatter()
// the date format that we will specify for the LLM to return
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.locale = Locale.current
if let date = date {
return dateFormatter.date(from: date)
}
return nil
}
var formattedTime: DateComponents? {
let dateFormatter = DateFormatter()
// the time format that we will specify for the LLM to return
dateFormatter.dateFormat = "HH:mm"
dateFormatter.locale = Locale.current
if let time = time {
guard let time = dateFormatter.date(from: time) else {
print("Invalid time format")
return nil
}
let calendar = Calendar.current
let components = calendar.dateComponents([.hour, .minute], from: time)
return components
}
return nil
}
}
Note that since we are relying on natural language input, our app customer might forget to include all the correct details or might even try to “hack” our app and put in nonsensical input. For this reason, we allow each Parameter to be optional. However, if the API returns certain fields as nil, we can simply prompt the user to provide the correct details.
For example, the user interaction with not enough data would be as follows:
User: "Book a table at The Italian Place for tomorrow evening at 7 PM.” App Reply (after receiving a nil value for the numberOfPeople parameter from the LLM): “I would be happy to book your reservation at The Italian Place on Sunday, May 26th at 7pm. Just let me know how many people the reservation is for and I’ll finalize the reservation.” User: “The reservation will be for four people”
Now you can further specify the JSON schema for the RestaurantBookingParameters
option by providing extra description of the parameters. The more information you provide to the LLM about each parameter, the more accurate result you will receive back.
do {
let restaurantBookingSchema: JSONSchema = try JSONSchema(
type: RestaurantBookingParameters.self,
description: "Information required to make a restaurant booking",
propertyDescriptions: [
"name": "The name of the restaurant",
"date": "The date of the restaurant booking in yyyy-MM-dd format. Should be a date with a year, month, day. NOTHING ELSE",
"time": "The time of the reservation in HH:mm format. Should include hours and minutes. NOTHING ELSE",
"number_of_people": "The total number of people the reservation is for"
],
required: false
)
} catch {
print(error)
}
You can also specify the scheme directly to avoid throwing errors:
let restaurantBookingSchema = JSONSchema(
type: .object,
description: "Information required to make a restaurant booking",
properties: [
"name": JSONSchema(
type: .string,
description: "The name of the restaurant",
required: false
),
"date" : JSONSchema(
type: .string,
description: "The date of the restaurant booking in yyyy-MM-dd format. Should be a date with a year, month, day. NOTHING ELSE",
required: false
),
"time" : JSONSchema(
type: .string,
description: "The time of the reservation in HH:mm format. Should include hours and minutes. NOTHING ELSE",
required: false
),
"number_of_people" : JSONSchema(
type: .integer,
description: "The total number of people the reservation is for",
required: false
)
],
// the required parameter specifies that each property is required
// note that you can also pass in an array of strings specifying the properties that are required as follows:
// required: ["name", "date", "time"]
required: false
)
_Note: since LLMs are mostly trained on Web APIs, it is ideal to use snake case (e.g. restaurant_booking) vs camel case as per Swift convention (e.g. restaurantBooking) for each property. However, for the RestaurantBookingParameters
object, the AI framework will automatically convert it to snake case before sending to the LLM API. _
The restaurantBookingSchema
will be used in the properties
parameter of the function call, which you can set with the key for retrieving the booking from the API result:
let restaurantBookingProperties: [String : JSONSchema] = ["restaurant_booking_parameters" : restaurantBookingSchema]
The "restaurant_booking_parameters"
key should also be set up within a Codable
object that will be decoded as the result object from the LLM API:
struct RestaurantBookingResult: Codable, Hashable, Sendable {
let restaurantBookingParameters: RestaurantBookingParameters
}
Now we can describe our original bookRestaurant
function to the LLM, including the JSONScheme of the input parameters that were specified in restaurantBookingProperties
:
let bookRestaurantFunction = AbstractLLM.ChatFunctionDefinition(
name: "book_restaurant",
context: "Make a restaurant booking",
parameters: JSONSchema(
type: .object,
description: "Required data to make a restaurant booking",
properties: restaurantBookingProperties
)
)
After specifying our function and its properties, we just need to set the system and user messages for the LLM to complete:
// Note that the LLM is trained with a cutoff date. So make sure to specify today's date in the system or user prompt for correct interpretation of relative time descriptions such as "tomorrow".
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
let dateString = dateFormatter.string(from: Date())
let messages: [AbstractLLM.ChatMessage] = [
.system {
"You are a helpful assistant tasked with booking restaurant reservations. A user wants to book a table. Please gather the following details efficiently: 1) Name of the restaurant, 2) Date of the reservation, 3) Time of the reservation, 4) Number of people attending. If the client doesn't provide a piece of information, simple return NULL. DO NOT ADD ANY ADDITIONAL INFORMATION. Today's date is \(dateString)"
},
.user {
"Book a table for four at The Italian Place for tomorrow evening."
}
]
We now have all the components to call the function:
// only OpenAI and Anthropic support functions at this time
let client = OpenAI.APIClient(apiKey: "YOUR_API_KEY")
do {
let functionCall: AbstractLLM.ChatFunctionCall = try await client.complete(
messages,
functions: [bookRestaurantFunction],
as: .functionCall
)
let result = try functionCall.decode(RestaurantBookingResult.self)
print(result.restaurantBookingParameters)
} catch {
print(error)
}
The Full Function Call
The final code will be as follows:
import AI
import OpenAI
import CorePersistence
// only OpenAI and Anthropic support functions at this time
let client = OpenAI.APIClient(apiKey: "YOUR_API_KEY")
// today's date
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
let dateString = dateFormatter.string(from: Date())
let messages: [AbstractLLM.ChatMessage] = [
.system {
"You are a helpful assistant tasked with booking restaurant reservations. A user wants to book a table. Please gather the following details efficiently: 1) Name of the restaurant, 2) Date of the reservation, 3) Time of the reservation, 4) Number of people attending. If the client doesn't provide a piece of information, simple return NULL. DO NOT ADD ANY ADDITIONAL INFORMATION. Today's date is \(dateString)"
},
.user {
"Book a table for four at The Italian Place for tomorrow evening."
}
]
struct RestaurantBookingParameters: Codable, Hashable, Sendable {
let name: String?
let date: String?
let time: String?
let numberOfPeople: Int?
var formattedDate: Date? {
let dateFormatter = DateFormatter()
// standard date format from OpenAI
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.locale = Locale.current
if let date = date {
return dateFormatter.date(from: date)
}
return nil
}
var formattedTime: DateComponents? {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm"
dateFormatter.locale = Locale.current
if let time = time {
guard let time = dateFormatter.date(from: time) else {
print("Invalid time format")
return nil
}
let calendar = Calendar.current
let components = calendar.dateComponents([.hour, .minute], from: time)
return components
}
return nil
}
}
let restaurantBookingSchema = JSONSchema(
type: .object,
description: "Information required to make a restaurant booking",
properties: [
"name": JSONSchema(
type: .string,
description: "The name of the restaurant",
required: false
),
"date" : JSONSchema(
type: .string,
description: "The date of the restaurant booking in yyyy-MM-dd format. Should be a date with a year, month, day. NOTHING ELSE",
required: false
),
"time" : JSONSchema(
type: .string,
description: "The time of the reservation in HH:mm format. Should include hours and minutes. NOTHING ELSE",
required: false
),
"number_of_people" : JSONSchema(
type: .integer,
description: "The total number of people the reservation is for",
required: false
)
],
required: false
)
let restaurantBookingProperties: [String : JSONSchema] = ["restaurant_booking_parameters" : restaurantBookingSchema]
let bookRestaurantFunction = AbstractLLM.ChatFunctionDefinition(
name: "book_restaurant",
context: "Make a restaurant booking",
parameters: JSONSchema(
type: .object,
description: "Required data to make a restaurant booking",
properties: restaurantBookingProperties
)
)
struct RestaurantBookingResult: Codable, Hashable, Sendable {
let restaurantBookingParameters: RestaurantBookingParameters
}
do {
let functionCall: AbstractLLM.ChatFunctionCall = try await client.complete(
messages,
functions: [bookRestaurantFunction],
as: .functionCall
)
let result = try functionCall.decode(RestaurantBookingResult.self)
print(result.restaurantBookingParameters)
} catch {
print(error)
}
The response will fit what we can parse from the natural language message "Book a table for four at The Italian Place for tomorrow evening at 7 PM."
RestaurantBookingParameters(
name: "The Italian Place",
date: "2024-05-26",
time: "19:00",
numberOfPeople: 4
)
If any of the parameters are nil, we can further prompt the user to provide this information, and concatenate their updated response to the original user prompt until we get all the parameters. Finally, we can use these structured parameters to pass to our local bookRestaurant
function, which will call our company’s API to make the actual restaurant reservation.
Using this Function Calling approach allows us to leverage the power of LLMs to convert natural language input into structured data. This structured data can then be used to interact with our private APIs while providing a more seamless and intuitive user experience at the UI level. Whether it's booking a restaurant or performing other complex tasks, the possibilities are endless.