Skip to content

Instantly share code, notes, and snippets.

@searls
Last active August 17, 2025 18:57
Show Gist options
  • Save searls/55aa3768d77f8e32d187273193f894b9 to your computer and use it in GitHub Desktop.
Save searls/55aa3768d77f8e32d187273193f894b9 to your computer and use it in GitHub Desktop.
import Foundation
import FoundationModels
// Phase 1: Define a structure to model a dynamic set of prompts that could be driven by arbitrary data (e.g. flat JSON file, fetched over HTTP, etc)
struct EducationalPromptSet {
let type: String
let instructions: String
let name: String
let description: String
let summaryGuideDescription: String
let confidenceGuideDescription: String
let subComponents: [SubComponentPromptSet]
}
struct SubComponentPromptSet {
let title: String
let bodyGuideDescription: String
}
// Phase 2: Instantiate (manually in this case, but most likely from a Decoder) an instance of a prompt set
let cocktailPromptSet = EducationalPromptSet(
type: "bartender_basic",
instructions: """
You are an expert bartender. Take the provided cocktail name or list of ingredients and explain how to make a delicious cocktail. Be creative!
""",
name: "Cocktail Recipe",
description: "A custom cocktail recipe, tailored to the user's input and communicated in an educational tone and spirit",
summaryGuideDescription: "The summary should describe the history (if applicable) and taste profile of the cocktail",
confidenceGuideDescription: "Range between 0-100 for your confidence in the feasibility of this cocktail based on the prompt",
subComponents: [
SubComponentPromptSet(title: "Ingredients", bodyGuideDescription: "A list of all ingredients in the cocktail"),
SubComponentPromptSet(title: "Steps", bodyGuideDescription: "A list of the steps to make the cocktail"),
SubComponentPromptSet(title: "Prep", bodyGuideDescription: "The bar prep you should have completed in advance of service"),
]
)
// Phase 3: Generate a schema from an instance of a prompt set
// 3.1 First, a separate schema for the unique prompts of each subComponent
let cocktailSubComponentSchemas = cocktailPromptSet.subComponents.enumerated().map { index, subComponentPromptSet in
DynamicGenerationSchema(
name: "subComponentSchema[\(index)]",
properties: [
DynamicGenerationSchema.Property(
name: "title",
description: subComponentPromptSet.title,
schema: DynamicGenerationSchema(type: String.self, guides: [
// Problem 1:
// There is no public constructor for GenerationGuide, so one cannot programmatically constrain output with a guide like the macro @Guide(.pattern(/Title Goes Here/))
// Or in this case Regex("^" + NSRegularExpression.escapedPattern(for: subComponentPromptSet.title) + "$")
//
// Is it silly that I want a property that is fixed to a known string and am asking an LLM to force itself to generate that string? Absolutely, but I can't think of
// any other way a result object would get that title back to me in the result object such that a result struct could conform to
// ConvertibleFromGeneratedContent (as `init(_ content: GeneratedContent)` can't reference the SubComponentPromptSet or DynamicGenerationSchema
//
// Something's weird here, because if you can't dynamically create generation guide, why does this constructor (DynamicGenerationSchema.init(type:guides:) even exist?
// https://developer.apple.com/documentation/foundationmodels/generationguide
])
),
DynamicGenerationSchema.Property(
name: "body",
description: subComponentPromptSet.bodyGuideDescription,
schema: DynamicGenerationSchema(type: String.self)
)
]
)
}
// 3.2 Then a schema mirroring the overall cocktail prompt set
let cocktailSchema = DynamicGenerationSchema(
name: cocktailPromptSet.name,
description: cocktailPromptSet.description,
properties: [
DynamicGenerationSchema.Property(
name: "summary",
description: cocktailPromptSet.summaryGuideDescription,
schema: DynamicGenerationSchema(type: String.self)
),
DynamicGenerationSchema.Property(
name: "confidence",
description: cocktailPromptSet.confidenceGuideDescription,
schema: DynamicGenerationSchema(type: Int.self)
)
] + cocktailSubComponentSchemas.enumerated().map { index, subComponentSchema in
DynamicGenerationSchema.Property(
name: "subComponentSchema[\(index)]",
schema: subComponentSchema
)
}
)
// Phase 4: Define result structs that can be instantiated from the model's GeneratedContent
struct EducationalResult : ConvertibleFromGeneratedContent {
let summary: String
let confidence: Int
let subComponents: [SubComponentResult]
init(_ content: GeneratedContent) throws {
summary = try content.value(String.self, forProperty: "summary")
confidence = try content.value(Int.self, forProperty: "confidence")
// Problem 2: By removing GeneratedContent#properties and GeneratedContent#elements in Beta 5, there's no longer any way to iterate over
// a set of dynamically-defined properties, so we have to resort to fetching them by inferred-name into an array, presumably with some max-supported number
subComponents = try (0...2).map { index in
// Problem 3: Even when generating pseudo-array element locators via String interpolation, attempting to fetch a
// non-existent property will throw error, so we have to know exactly how many such properties are defined up-front, which
// (as before) is impossible to know in a ConvertibleFromGeneratedContent-compliant constructor, since we lack a reference to
// cocktailPromptSet or cocktailSchema
let subComponentContent = try content.value(GeneratedContent.self, forProperty: "subComponentSchema[\(index)]")
return try SubComponentResult(subComponentContent)
}
}
}
struct SubComponentResult : ConvertibleFromGeneratedContent {
let title: String
let body: String
init(_ content: GeneratedContent) throws {
title = try content.value(String.self, forProperty: "title")
body = try content.value(String.self, forProperty: "body")
}
}
import Playgrounds
#Playground {
let session = LanguageModelSession {
cocktailPromptSet.instructions
}
let response = try await session.respond(
to: "Shirley Temple",
schema: GenerationSchema(root: cocktailSchema, dependencies: [])
)
let cocktailResult = try EducationalResult(response.content)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment