Generating Custom Data Structures
Generating Custom Data Structures
To explore the value of guided generation, first consider the case of developing a data structure without it. Run the app and enter the following prompt:
Create a menu for lunch at a casual dining restaurant
The result will be a plausible menu that is also a wall of text. For a case where the user only needs to read the result, this works fine. Imagine a scenario where you want to provide a realistic menu in a game when the player enters a restaurant. You would need to modify and refine your prompts to create a format that can be interpreted as a structure. Instead, you will let Foundation Models do this work for you.
In the app, tap the second item under the new menu, Dining Menu. Currently, it contains only a single button labeled Generate a Lunch Menu that does nothing. You’ll change this view to use Foundation Models to generate example menus using a custom data structure.
Before using guided generation, you must first define the data structures to fill with the generated information. Move to the Models folder. You’ll see the familiar Message structure along with some other files that you’ll work with later in this lesson when discussing tools. For now, create a new Swift file in this folder named RestaurantMenu.swift. Open it and change the code to:
import Foundation
import FoundationModels
enum MealType: String {
case breakfast = "Breakfast"
case lunch = "Lunch"
case dinner = "Dinner"
}
struct MenuItem {
let name: String
let description: String
let ingredients: [String]
let cost: Decimal
}
struct RestaurantMenu {
let type: MealType
let menu: [MenuItem]
}
This creates an enumeration for the various meals of the day. It also defines a struct that provides information about each item on the menu and the RestaurantMenu to store the menu items along with the meal type for the generated menu. The framework provides two macros that assist the model and guide the generated data. The first is the Generable(description:) macro, which marks structures and enumerations for guided generation and provides context to the model. You will use it along with the Guide(description:) macro on each property in these Generable types. The framework allows nesting Generable types to support complex data hierarchies.
Note: Since you already imported
FoundationModels, you can now use theGenerable(description:)andGuide(description:)macros without adding any new imports.
The Generable(description:) macro is required for any type that you wish to create using Foundation Models. To see this, add the following code above the definition of RestaurantMenu:
@Generable(description: "A menu of offerings for a restaurant for a single meal.")
If you attempt to build the app after this change, you will encounter several compilation errors. The errors all result from the requirement that any property inside a Generable struct must also be a Generable type. As already noted, the basic Swift types already meet this requirement, but both the type and menu properties are of a custom type, so you must also make them Generable. Add the following code above the definition of the MealType enumerable:
@Generable
and the following code above the definition of the MenuItem struct:
@Generable(description: "A single dish for a restaurant menu.")
Building the app now will no longer produce errors as the types inside the MenuItem struct already support Generable. The description parameter with MenuItem and RestaurantMenu provides the model with semantics and context for the data. Try to keep descriptions as short as possible, as long descriptions take up additional context size and increase latency when generating the data.
It’s important to note that the properties will be generated in the order they are declared within the Swift struct. This ordering can subtly influence the model’s data production. In the MenuItem struct, the description property precedes the ingredients property. The model then creates the description before providing a list of ingredients. Both are generated after the first name property. Providing this order generates the name first, which is then followed by a description that fits the already produced name. The model then generates ingredients to match the name and description of the menu item. Finally, the cost should accurately reflect the other properties of the menu item. If you had started the struct with a property such as the cost, then the cost would influence the others.
While the property names do give helpful information on what the struct should contain, you can use the Guide(description:) macro on each property. From the function signature, you can see it supports a description to provide context to the model or a prompt the model can use to generate that property.
Update the MenuItem definition to:
@Generable(description: "A single dish for a restaurant menu.")
struct MenuItem {
@Guide(description: "Name for this dish.")
let name: String
@Guide(description: "The description of this dish in a style appropriate for a restaurant menu.")
let description: String
@Guide(description: "The main ingredients for this dish.")
let ingredients: [String]
@Guide(description: "A cost for this dish in US dollars, which should be appropriate for the ingredients", )
let cost: Decimal
}
This code describes each property. As with many aspects of LLMs, prompts are as much an art as a science, but they clarify the purpose of each property and how they integrate.
You can also provide more specific guidance to the model using the @Guide macro. Update the RestaurantMenu definition to:
@Generable(description: "A menu of offerings for a restaurant for a single meal.")
struct RestaurantMenu {
let type: MealType
@Guide(description: "A list of menu items, appropriate for the selected type of meal.", .count(4...8))
let menu: [MenuItem]
}
The .count(4...8) parameter to @Guide is one of several you can give the macro. When applied to an array, this code specifies that the menu should contain four to eight items, inclusive. In general, the count(_:) parameter ensures an array includes a specified number of elements. There are several more common properties to add restrictions for generated data:
- The
anyOf(_:)parameter restricts a property’s value to one of a defined array of options. The format would resemble@Guide(.anyOf(["Apple", "Banana", "Grape", "Strawberry"])). - For
Stringproperties, you can specify thepattern(_:)parameter that ensures the string follows a specified regular expression. - The
Inttype allows you to specifyminimum(_:)ormaximum(_:)values or arange(_:)to constrain the value.
These can be specified in addition to or instead of the description. This example applied both the description and count(_:) in one macro. They could also be split into two macros, both applied to the immediately following property.
That’s all the work needed to allow Foundation Models to generate a restaurant menu. To see it in action, open the MenuItemView.swift file and change the view to:
import SwiftUI
import FoundationModels
struct MenuItemView: View {
var menuItem: MenuItem
var body: some View {
HStack {
Text(menuItem.name)
Spacer()
Text(menuItem.cost, format: .currency(code: "USD"))
}
.font(.title)
Text(menuItem.description)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 15.0)
.font(.headline)
Text(menuItem.ingredients.joined(separator: " • "))
.font(.subheadline)
}
}
#Preview {
let item = MenuItem(
name: "Caesar Salad",
description: "Romaine lettuce tossed in Caesar dressing with parmesan cheese and croutons.",
ingredients: ["romaine lettuce", "Caesar dressing", "parmesan cheese", "croutons"],
cost: 10.0
)
MenuItemView(menuItem: item)
}
This view displays information about a MenuItem struct. To display the cost, use the format parameter and pass the .currency(code: "USD") formatter. You also use the joined(separator:) method on the array of strings containing the ingredients to format it as a single string with each ingredient separated by a dot.
Now open FoodMenuView.swift. Add the following new property to the top of the view:
@State var menu: RestaurantMenu?
This state property will store the menu once it is created. Make sure you import FoundationModels at the top of the file, since it provides the LanguageModelSession capabilities you’ll use next. Next, add the following new method above the view and after the menu property:
// 1
func generateLunchMenu() async {
// 2
let session = LanguageModelSession(instructions: "You are a helpful model assisting with generating realistic restaurant menus.")
// 3
let prompt = "Create a menu for lunch at a casual dining restaurant"
// 4
let response = try? await session.respond(to: prompt, generating: RestaurantMenu.self)
// 5
menu = response?.content
}
Here’s how this works:
- You mark the new method as
asyncsince it contains asynchronous code. - Inside the method, you create a new
LanguageModelSessionand provide it instructions appropriate to the task the session will perform. - You create a prompt that provides the information about the response you want to make. Notice this specifies the type of restaurant and produces a lunch menu. Changing the prompt will create menus for different meals or different restaurants.
- You get a response as before, now passing the
generatingproperty the valueRestaurantMenu.self, which instructs the model to produce an object of theRestaurantMenutype. You use thetry? awaitpattern to generate anilresponse if anything goes wrong. - This code sets the
menuproperty you created to the generated Foundation Model response. If anything went wrong in step four, this will benil. Otherwise it should contain a menu of four to eight items as specified using the@Guidemacro.
You need to run this code when the user taps the button. Replace the empty action in the button with:
Task {
await generateLunchMenu()
}
When the user taps the Create Lunch Menu button, it will now call the method wrapped inside a Task closure to handle the asynchronous task. Finally, add the following code to show the menu after the Button and before the Spacer:
if let menu = menu {
ScrollView {
ForEach(menu.menu, id: \.name) { item in
MenuItemView(menuItem: item)
Divider()
}
}
}
This code attempts to unwrap the menu property. When not nil, it loops through each item in the menu and display it using the MenuItemView view you created earlier in this section. The Divider view separates each item in the menu. Run the app, tap the menu and select the Dining Menu. Now tap Generate Lunch Menu and after a few seconds, you should see the generated menu.
You’ve learned how to create data structures with guided generation. This example waits until the full data structure exists before showing it to the user. As with text responses, you can also stream the response to produce a better user experience. You’ll learn that in the next section.