18 KiB
Advanced
Custom Adapters
Loading and Using Custom Adapters
Foundation Models supports custom adapters that specialize the base model for specific use cases without retraining the entire model from scratch. Adapters are trained by you to modify the model's behavior while maintaining its core capabilities.
Training an Adapter
Before you can load a custom adapter, you first need to train one with an adapter training toolkit.
The toolkit uses Python and PyTorch and requires familiarity with training machine-learning models. After training, use the toolkit to export the adapter in a format compatible with Foundation Models.
Requirements:
- Python 3.8+
- PyTorch
- Training data specific to your use case
- Familiarity with ML model training
Deploying Adapters
Important: Adapter files are large (160 MB or more), so don't bundle them in your app.
Deployment Options:
- App Store Connect - Host via on-demand resources
- Your Server - Download on-demand based on user needs
- Local Development - Load from file URL during testing
SystemLanguageModel.Adapter
Load an adapter from a local file URL:
// The absolute path to your adapter
let localURL = URL(filePath: "absolute/path/to/my_adapter.fmadapter")
// Initialize the adapter by using the local URL
let adapter = try SystemLanguageModel.Adapter(fileURL: localURL)
// Create model instance with the adapter
let customAdapterModel = SystemLanguageModel(adapter: adapter)
// Create a session and prompt the model
let session = LanguageModelSession(model: customAdapterModel)
let response = try await session.respond(to: "Your prompt here")
Requirements
To use custom adapters:
- Entitlement Required: Add
com.apple.developer.foundation-model-adaptercapability - Train Adapter: Use Apple's adapter training toolkit (Python/PyTorch)
- Export Adapter: Convert to
.fmadapterformat - Deploy Adapter: Host remotely (App Store or your server)
- Download On-Demand: Fetch when needed (don't bundle in app)
Checking Adapter Availability
guard SystemLanguageModel.isAvailable else {
print("Model not available on this device")
return
}
// Check if adapter file exists
let adapterURL = getAdapterURL() // Your method to get/download adapter
guard FileManager.default.fileExists(atPath: adapterURL.path) else {
print("Adapter file not found")
return
}
// Initialize with adapter
do {
let adapter = try SystemLanguageModel.Adapter(fileURL: adapterURL)
let model = SystemLanguageModel(adapter: adapter)
let session = LanguageModelSession(model: model)
} catch {
print("Failed to load adapter: \(error)")
}
Use Cases for Custom Adapters
- Domain-Specific Language: Medical, legal, or technical terminology
- Brand Voice: Consistent tone and style for your brand
- Specialized Tasks: Custom classification or extraction
- Language Variants: Dialects or industry-specific language
Transcript Management
Transcript Struct
The Transcript struct represents a linear history of interactions with the model.
Purpose:
- Save and restore conversation history
- Export conversations
- Analyze interaction patterns
- Implement conversation replay
Creating Transcripts
// Session automatically maintains a transcript
let session = LanguageModelSession(model: model)
// Access the transcript
let currentTranscript = session.transcript
// Save transcript for later
let transcriptData = try JSONEncoder().encode(currentTranscript)
UserDefaults.standard.set(transcriptData, forKey: "savedTranscript")
Restoring from Transcript
// Load saved transcript
guard let transcriptData = UserDefaults.standard.data(forKey: "savedTranscript"),
let transcript = try? JSONDecoder().decode(Transcript.self, from: transcriptData) else {
return
}
// Create session from transcript
let session = LanguageModelSession(
model: model,
tools: [],
transcript: transcript
)
// Continue conversation from where it left off
let response = try await session.respond(to: Prompt("Continue our discussion"))
Transcript Structure
struct Transcript: Codable {
let entries: [Entry]
struct Entry: Codable {
let timestamp: Date
let segment: Segment
}
enum Segment: Codable {
case instructions(Instructions)
case prompt(Prompt)
case response(Response)
case toolCall(ToolCall)
case toolResult(ToolResult)
}
}
Analyzing Transcripts
func analyzeConversation(_ transcript: Transcript) {
let promptCount = transcript.entries.filter { entry in
if case .prompt = entry.segment { return true }
return false
}.count
let responseCount = transcript.entries.filter { entry in
if case .response = entry.segment { return true }
return false
}.count
let toolCallCount = transcript.entries.filter { entry in
if case .toolCall = entry.segment { return true }
return false
}.count
print("Conversation stats:")
print("- Prompts: \(promptCount)")
print("- Responses: \(responseCount)")
print("- Tool calls: \(toolCallCount)")
}
LanguageModelFeedback
Overview
The LanguageModelFeedback struct provides a way to log issues and provide feedback about model responses. This helps improve the model over time and can be attached to Feedback Assistant reports.
Recording Feedback
// After receiving a response
let response = try await session.respond(to: prompt)
// If the response has issues, log feedback
let feedback = try await session.logFeedbackAttachment(
sentiment: .negative,
issues: [.inaccurate, .unhelpful],
desiredOutput: "The model should have provided more specific Swift code examples"
)
// Attach to Feedback Assistant
// (Feedback Assistant integration code)
LanguageModelFeedback.Sentiment
Represents your overall sentiment about the response:
enum Sentiment {
case positive // Response was helpful and accurate
case neutral // Response was acceptable
case negative // Response had significant issues
}
LanguageModelFeedback.Issue
Specific issues with the model's response:
enum Issue {
case inaccurate // Factually incorrect information
case unhelpful // Didn't address the question
case inappropriate // Violated content policies
case incomplete // Missing important information
case irrelevant // Off-topic response
case formatIncorrect // Didn't follow requested format
case toolCallFailed // Tool calling didn't work as expected
}
Complete Feedback Example
func handleResponse(prompt: Prompt, response: String, session: LanguageModelSession) async {
// Evaluate response quality
let isGoodResponse = evaluateResponse(response)
if !isGoodResponse {
do {
let feedback = try await session.logFeedbackAttachment(
sentiment: .negative,
issues: [.inaccurate, .incomplete],
desiredOutput: """
Expected a comprehensive answer with:
1. Clear explanation
2. Code examples
3. Best practices
"""
)
// Log for analytics
print("Feedback logged: \(feedback)")
// Optionally show user feedback UI
await showFeedbackConfirmation()
} catch {
print("Failed to log feedback: \(error)")
}
}
}
func evaluateResponse(_ response: String) -> Bool {
// Your evaluation logic
return response.count > 50 && !response.contains("I don't know")
}
Automatic Feedback Collection
class FeedbackCollector {
func collectFeedbackAutomatically(
session: LanguageModelSession,
prompt: Prompt,
response: String
) async throws {
// Analyze response quality
let hasCodeExamples = response.contains("```")
let isSubstantive = response.count > 100
let hasStructure = response.contains("\n\n")
let sentiment: LanguageModelFeedback.Sentiment
var issues: [LanguageModelFeedback.Issue] = []
if hasCodeExamples && isSubstantive && hasStructure {
sentiment = .positive
} else {
sentiment = .neutral
if !hasCodeExamples {
issues.append(.incomplete)
}
if !isSubstantive {
issues.append(.unhelpful)
}
}
if !issues.isEmpty {
_ = try await session.logFeedbackAttachment(
sentiment: sentiment,
issues: issues,
desiredOutput: "More comprehensive response with examples"
)
}
}
}
User-Initiated Feedback
struct FeedbackView: View {
let session: LanguageModelSession
let prompt: Prompt
let response: String
@State private var selectedIssues: Set<LanguageModelFeedback.Issue> = []
@State private var desiredOutput: String = ""
var body: some View {
Form {
Section("What went wrong?") {
Toggle("Inaccurate", isOn: binding(for: .inaccurate))
Toggle("Unhelpful", isOn: binding(for: .unhelpful))
Toggle("Incomplete", isOn: binding(for: .incomplete))
Toggle("Irrelevant", isOn: binding(for: .irrelevant))
}
Section("What did you expect?") {
TextEditor(text: $desiredOutput)
.frame(height: 100)
}
Button("Submit Feedback") {
Task {
try await session.logFeedbackAttachment(
sentiment: .negative,
issues: Array(selectedIssues),
desiredOutput: desiredOutput
)
}
}
}
}
private func binding(for issue: LanguageModelFeedback.Issue) -> Binding<Bool> {
Binding(
get: { selectedIssues.contains(issue) },
set: { isSelected in
if isSelected {
selectedIssues.insert(issue)
} else {
selectedIssues.remove(issue)
}
}
)
}
}
Performance Optimization
Prewarming Sessions
Reduce latency by preloading the model and caching prompt prefixes:
// Prewarm the session with common prefix
let session = LanguageModelSession(
model: model,
instructions: Instructions("You are a helpful coding assistant")
)
// Prewarm with expected prompt prefix
try await session.prewarm(promptPrefix: "Explain how to use ")
// Subsequent prompts with this prefix will be faster
let response = try await session.respond(
to: Prompt("Explain how to use async/await in Swift")
)
Session Reuse
class ConversationManager {
private var cachedSession: LanguageModelSession?
private let model: SystemLanguageModel
init(model: SystemLanguageModel) {
self.model = model
}
func getSession() async throws -> LanguageModelSession {
if let session = cachedSession {
return session
}
let session = LanguageModelSession(model: model)
cachedSession = session
// Prewarm for faster first response
try await session.prewarm()
return session
}
func resetSession() {
cachedSession = nil
}
}
Best Practices
1. Monitor Token Usage
let session = LanguageModelSession(model: model)
print("Token limit: \(model.sessionTokenLimit)")
// Track approximate token usage
var estimatedTokens = 0
estimatedTokens += prompt.text.count / 4 // Rough estimation
estimatedTokens += response.count / 4
if estimatedTokens > model.sessionTokenLimit * 0.8 {
// Approaching limit, consider starting new session
print("Warning: Approaching token limit")
}
2. Handle Long Conversations
func manageLongConversation() async throws {
var session = LanguageModelSession(model: model)
var turnCount = 0
while true {
let userInput = await getUserInput()
do {
let response = try await session.respond(to: Prompt(userInput))
await displayResponse(response)
turnCount += 1
// Reset session every 10 turns to avoid token limits
if turnCount >= 10 {
// Save important context
let summary = try await session.respond(
to: Prompt("Summarize our conversation so far")
)
// Start fresh with summary as context
session = LanguageModelSession(
model: model,
instructions: Instructions("Previous conversation summary: \(summary)")
)
turnCount = 0
}
} catch {
print("Error: \(error)")
}
}
}
3. Graceful Degradation
func respondWithFallback(prompt: Prompt) async -> String {
do {
let session = try await model.session()
return try await session.respond(to: prompt)
} catch {
// Fallback to simpler response
return "I apologize, but I'm unable to process your request right now. Please try again."
}
}
Real-World Example: Game Character Dialog
Complete example from Apple's sample app showing dynamic game content generation with guided generation and tools.
Character Definition
protocol Character {
var id: UUID { get }
var displayName: String { get }
var firstLine: String { get }
var persona: String { get }
}
struct Barista: Character {
let id = UUID()
let displayName = "Barista"
let firstLine = "Hey there. Can you get the dream orders?"
let persona = """
Chike is the head barista at Dream Coffee, and loves serving up
the perfect cup of coffee to all the dreamers and creatives.
Chike is friendly, knowledgeable about coffee, and enjoys making
connections with customers.
"""
}
Multi-Turn Conversation System
let instructions = """
A multiturn conversation between a game character and the player. \
You are \(character.displayName). Refer to \(character.displayName) in \
the first-person (like "I" or "me"). You must respond in the voice of \
\(character.persona).
Keep your responses brief - 2-3 sentences maximum.
Stay in character at all times.
Reference previous conversation context when appropriate.
"""
let session = LanguageModelSession(
model: model,
instructions: Instructions(instructions)
)
// Player can have ongoing conversations
let response1 = try await session.respond(to: Prompt("Hello!"))
let response2 = try await session.respond(to: Prompt("What's your favorite coffee?"))
let response3 = try await session.respond(to: Prompt("Can you make me one?"))
Procedurally Generated Characters
@Generable(description: "A procedurally generated customer")
struct ProceduralCustomer: Character {
var id = UUID()
@Guide(description: "A unique, creative name")
var displayName: String
@Guide(description: "Opening line that introduces their personality")
var firstLine: String
@Guide(description: "2-3 sentence personality description")
var persona: String
@Guide(description: "Their favorite type of coffee or drink")
var favoriteDrink: String
}
// Generate unique customers dynamically
let prompt = Prompt("Create a quirky regular customer who visits this coffee shop")
let customer = try await session.respond(
to: prompt,
generating: ProceduralCustomer.self
)
// Each customer is unique!
print(customer.displayName) // e.g., "Luna the Dreamer"
print(customer.firstLine) // e.g., "Is it morning already? I was up all night painting..."
Combining Tools with Game State
struct OrderDatabaseTool: Tool {
let name = "checkOrderStatus"
let description = "Checks the status of a customer's coffee order"
@Generable
struct Arguments {
@Guide(description: "The customer's name")
var customerName: String
}
func call(arguments: Arguments) async throws -> String {
// Query game state
if let order = GameState.shared.getOrder(for: arguments.customerName) {
return "Order: \(order.drinkType), Status: \(order.status)"
}
return "No order found for \(arguments.customerName)"
}
}
let session = LanguageModelSession(
model: model,
tools: [OrderDatabaseTool()],
instructions: Instructions(baristaInstructions)
)
// Character can reference actual game state
let response = try await session.respond(
to: Prompt("Is Luna's latte ready yet?")
)
// Model calls OrderDatabaseTool automatically and incorporates result
Summary
Advanced Features:
- Custom Adapters - Specialize the model for your domain (160MB+, deploy remotely)
- Transcript Management - Save, restore, and analyze conversations
- Feedback System - Improve model quality with structured feedback
- Performance Optimization - Prewarming and session management
- Token Management - Monitor and handle token limits
- Error Handling - Graceful degradation strategies
- Game Content - Dynamic dialog, procedural generation, multi-turn conversations
These advanced features enable production-ready implementations with Foundation Models, including rich interactive experiences like games, chatbots, and personalized content.