Dynamic Guided Generation
Dynamic Guided Generation
The Generable macro works very well when you know the structure of your data at compile time. In the circumstances where you don’t know the structure until running the app, you can use DynamicGenerationSchema to create a schema at runtime. This produces a result similar to what you’ve done. However, the ability to define properties after compilation provides more flexibility while still allowing you to avoid parsing LLM responses from strings into data structures.
One way to expand the menu generation you’ve created would be to add the ability for users to specify their own ingredients. If you knew them in advance, you could specify them at compile time using the .anyOf(_:) parameter on an array. You will instead create a dynamically generated schema for the special of the day, based on a menu made from one of a set of specified ingredients.
Open FoodMenuView and add the following property to hold a list of user-provided ingredients:
@State var ingredients: String = "lamb, salmon, duck"
Now add the following code before the Button:
Text("Comma separated list of possible ingredients.")
TextField("Ingredients for Special", text: $ingredients)
This displays instructions along with a textbox to hold a comma-separated list of ingredients. To convert the comma-separated text into an array of strings, add the following computed property after ingredients:
var ingredientArray: [String] {
let array = ingredients.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
if array.isEmpty {
return ["lamb", "salmon", "duck"]
} else {
return array
}
}
This code attempts to split the provided ingredients into an array of strings at each comma using the split(separator:maxSplits:omittingEmptySubsequences:) method. It then uses the map(_:) function to trim any whitespace from each ingredient. If the array is empty, it returns a default array; otherwise, it returns the generated array. Add a new method to generate the special menu item by adding the following code after generateLunchMenu():
func generateMenuSpecial() {
// 1
let specialMealSchema = DynamicGenerationSchema(
name: "specialmenuitem",
// 2
properties: [
// 3
DynamicGenerationSchema.Property(
name: "ingredients",
// 4
schema: DynamicGenerationSchema(
name: "ingredients",
anyOf: ingredientArray
)
),
// 5
DynamicGenerationSchema.Property(
name: "name",
schema: DynamicGenerationSchema(type: String.self)
),
DynamicGenerationSchema.Property(
name: "description",
schema: DynamicGenerationSchema(type: String.self)
),
DynamicGenerationSchema.Property(
name: "price",
schema: DynamicGenerationSchema(type: Decimal.self)
)
]
)
}
Here’s how this code works:
- To create a dynamic schema, you first create a
DynamicGenerationSchemaobject and give it a name. - You then define the properties of this schema. This is the equivalent of the properties of the struct that you created when using the
@Generablemacro. - The first property you define is the
ingredients. Recall that guided generation fills in properties in the order you specify them. Since you want the ingredient to define the menu item, you specify it first. - The schema parameter of the
DynamicGenerationSchemacall states the data type of the property. In this case, you make another call toDynamicGenerationSchemawith the same name and provide theanyOfparameter, passing in the array of strings computed asingredientArray. The result will select one of the ingredients provided at runtime. This ability cannot be done using the@Generablemacro. - The remaining parameters should look familiar. You provide the name for each and set the schema using the
DynamicGenerationSchemainitializer with thetypeproperty, specifying the appropriate simple type for each.
This DynamicGenerationSchema defines the same structure as earlier, but with the ingredient selected from a list provided by the user at runtime from the view.
Now add the following code to the end of the generateMenuSpecial method:
// 1
let schema = try? GenerationSchema(root: specialMealSchema, dependencies: [])
// 2
guard let schema = schema else { return }
// 3
let session = LanguageModelSession(instructions: "You are a helpful model assisting with generating realistic restaurant menus.")
let specialPrompt = "Produce a lunch special menu item that is focused on the specified ingredient."
let response = try? await session.respond(to: specialPrompt, schema: schema)
Most of this code should be familiar at this point:
- You first convert the dynamic schema to a
GenerationSchemaby callingGenerationSchemaand passing yourDynamicGenerationSchemaas therootparameter. - When you try to create a generation schema, it can throw an error if there are conflicting property names, undefined references, or duplicate types. If any of those occur, then the
schemavariable will benil. You attempt to unwrapschemaand if that fails, you return from the method since something went wrong. - The remaining three lines create a
LanguageModelSessionwith the same instructions as earlier. You then provide a prompt and get a response from Foundation Models, passing in the unwrapped schema from step one to theschemaparameter. The response will be of typeGeneratedContentaccessible using thecontentproperty on the response.
Finish the method with the following code:
let name = try? response?.content.value(String.self, forProperty: "name")
let ingredients = try? response?.content.value(String.self, forProperty: "ingredients")
let description = try? response?.content.value(String.self, forProperty: "description")
let price = try? response?.content.value(Decimal.self, forProperty: "price")
let specialItem = MenuItem(
name: name ?? "",
description: description ?? "",
ingredients: ingredients == nil ? [] : [ingredients!],
cost: price ?? 0.0
)
special = specialItem
To get each property in the generated content, you call the value(_:forProperty:) method on the GeneratedContent. Note that this uses the try? pattern to return nil if anything goes wrong. You pass the expected type to value(_:forProperty:) along with the name of the property as you defined when creating the schema. The type should match the type specified when creating the schema.
You then create a MenuItem named specialItem from these properties, using the nil-coalescing operator to provide values if a property is nil. The method then assigns the generated data to a property named special. To add this state property, add the following after the menu property:
@State var special: MenuItem?
Now add the following code to call the method from the Button action after await generateLunchMenu():
await generateMenuSpecial()
To finish up the view, add the following code after the Button view and before the attempt to unwrap the menu property and display the special when available:
if let special = special {
MenuItemView(menuItem: special.asPartiallyGenerated())
Text("Today's Special")
.font(.title2)
Divider()
}
This attempts to unwrap the special state property. If successful, it will show the special item using the MenuItemView view along with the asPartiallyGenerated() method to convert the MenuItem to the partially generated version expected by the view.
Run the app, go to the Dining Menu view and generate a lunch menu. The regular menu will be generated as before. A few seconds after that, you will see the special menu item generated.
Now that you’ve explored guided generation, you will wrap up this module by examining the addition of external tools to Foundation Models.