A collection of tools and utilities for Apple's Foundation Models Framework that help working with the language model, better.
- Overview
- Features
- Requirements
- Installation
- Quick Start
- Configuration
- Tool Reference
- Utilities
- Usage Examples
- Advanced Topics
- Contributing
- License
FoundationModelsTools provides a set of pre-built tools and utilities that extend the capabilities of models using Apple's Foundation Models Framework. These tools allow you to:
- Access and manage calendar events
- Read and create contacts
- Get health data from HealthKit
- Access location services
- Control music playback
- Manage reminders
- Fetch weather information
- Extract metadata from web pages
- Search the web using Exa
- Manage context windows with token counting utilities
- Native Apple Framework Integration: Works seamlessly with EventKit, Contacts, HealthKit, CoreLocation, MapKit, and MusicKit
- Modern Swift Concurrency: Built with async/await patterns throughout
- Type-Safe: Leverages Swift's type system with the
@Generableprotocol - Permission Handling: Automatic authorization checks for privacy-sensitive operations
- Cross-Platform: Supports both iOS and macOS
- Comprehensive Error Handling: Detailed error messages for troubleshooting
- macOS 26.0+
- iOS 26.0+
- Swift 6.2+
- Xcode 26.0+
Add FoundationModelsTools as a dependency in your Package.swift:
dependencies: [
.package(url: "https://github.com/rudrankriyam/FoundationModelsTools", from: "0.1.0")
]Then add it to your target dependencies:
.target(
name: "YourTarget",
dependencies: ["FoundationModelsTools"]
)Here's a simple example to get you started:
import FoundationModels
import FoundationModelsTools
// Create a weather tool
let weatherTool = WeatherTool()
// Call it directly
let arguments = WeatherTool.Arguments(city: "San Francisco")
let result = try await weatherTool.call(arguments: arguments)
// Access the results
let temperature = result.value(Double.self, forProperty: "temperature")
let condition = result.value(String.self, forProperty: "condition")
print("It's \(temperature)°C and \(condition)")Add the required usage descriptions to your Info.plist for the tools you plan to use:
<!-- Calendar Access -->
<key>NSCalendarsUsageDescription</key>
<string>This app needs access to your calendar to create and manage events</string>
<!-- Contacts Access -->
<key>NSContactsUsageDescription</key>
<string>This app needs access to your contacts to search and create contact entries</string>
<!-- Health Data Access -->
<key>NSHealthShareUsageDescription</key>
<string>This app needs to read your health data</string>
<!-- Location Access -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs your location to provide location-based services</string>
<!-- Apple Music Access -->
<key>NSAppleMusicUsageDescription</key>
<string>This app needs access to Apple Music to control playback</string>
<!-- Reminders Access -->
<key>NSRemindersUsageDescription</key>
<string>This app needs access to reminders to create and manage tasks</string>WebTool requires an Exa API key.
⚠️ SECURITY WARNINGDo note store API keys directly in your app code or use
@AppStoragefor your production apps. API keys stored client-side can be extracted from your app bundle and misused.Recommended approach: Make API requests from a secure server where the API key is stored as an environment variable. Your app should call your server endpoint, which then makes the request to Exa's API.
For development/testing only, you can configure the key using @AppStorage:
import SwiftUI
@main
struct MyApp: App {
@AppStorage("exaAPIKey") private var exaAPIKey = ""
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
// DEVELOPMENT ONLY - Never ship API keys in production
// Get your key from: https://exa.ai
exaAPIKey = "your-api-key-here"
}
}
}
}Production setup:
- Create a server endpoint (e.g.,
/api/search) - Store your Exa API key in environment variables on the server
- Your app calls your server endpoint with the search query
- Your server makes the request to Exa's API and returns the results
Access and manage calendar events using EventKit.
Name: manageCalendar
Actions: create, query, read, update
Requirements:
- Calendar entitlement
- User permission via
NSCalendarsUsageDescription
Arguments:
action: The operation to performtitle: Event title (required for create)startDate: Start date inYYYY-MM-DD HH:mm:ssformatendDate: End date inYYYY-MM-DD HH:mm:ssformatlocation: Event locationnotes: Additional notescalendarName: Specific calendar to use (optional)daysAhead: Number of days to query (default: 7)eventId: Event identifier for read/update operations
Example:
let calendarTool = CalendarTool()
// Create an event
let createArgs = CalendarTool.Arguments(
action: "create",
title: "Team Meeting",
startDate: "2025-11-15 14:00:00",
endDate: "2025-11-15 15:00:00",
location: "Conference Room A",
notes: "Discuss Q4 planning"
)
let result = try await calendarTool.call(arguments: createArgs)
// Query upcoming events
let queryArgs = CalendarTool.Arguments(
action: "query",
daysAhead: 7
)
let events = try await calendarTool.call(arguments: queryArgs)
print(events.value(String.self, forProperty: "events"))Returns:
status: "success" or "error"eventId: Unique event identifiertitle: Event titlestartDate: Formatted start dateendDate: Formatted end datelocation: Event locationcalendar: Calendar name
Search, read, and create contacts using the Contacts framework.
Name: manageContacts
Actions: search, read, create
Requirements:
- Contacts entitlement
- User permission via
NSContactsUsageDescription
Arguments:
action: The operation to performname: Name to search for (for search action)contactId: Contact identifier (for read action)firstName: First name (for create action)lastName: Last name (for create action)phoneNumber: Phone number (for create action)email: Email address (for create action)organization: Company/organization (for create action)
Example:
let contactsTool = ContactsTool()
// Search for contacts
let searchArgs = ContactsTool.Arguments(
action: "search",
name: "John"
)
let results = try await contactsTool.call(arguments: searchArgs)
// Create a new contact
let createArgs = ContactsTool.Arguments(
action: "create",
firstName: "Jane",
lastName: "Doe",
email: "jane.doe@example.com",
phoneNumber: "+1234567890",
organization: "Acme Corp"
)
let contact = try await contactsTool.call(arguments: createArgs)Returns:
status: "success" or "error"contactId: Unique contact identifiergivenName: First namefamilyName: Last namefullName: Combined full nameemails: Array of email addressesphoneNumbers: Array of phone numbersorganization: Company name
Read health data from HealthKit including steps, heart rate, workouts, and more.
Name: accessHealth
Data Types: steps, heartRate, workouts, sleep, activeEnergy, distance
Requirements:
- HealthKit capability
- User permission via
NSHealthShareUsageDescription
Arguments:
dataType: Type of health data to querystartDate: Start date inYYYY-MM-DDformat (optional, defaults to 7 days ago)endDate: End date inYYYY-MM-DDformat (optional, defaults to today)
Example:
let healthTool = HealthTool()
// Query steps for the last 7 days
let stepsArgs = HealthTool.Arguments(
dataType: "steps",
startDate: "2025-11-07",
endDate: "2025-11-14"
)
let steps = try await healthTool.call(arguments: stepsArgs)
print("Total steps: \(steps.value(Int.self, forProperty: "totalSteps"))")
// Query heart rate
let heartArgs = HealthTool.Arguments(dataType: "heartRate")
let heartRate = try await healthTool.call(arguments: heartArgs)
// Query workouts
let workoutsArgs = HealthTool.Arguments(
dataType: "workouts",
startDate: "2025-11-01",
endDate: "2025-11-14"
)
let workouts = try await healthTool.call(arguments: workoutsArgs)Returns (varies by data type):
status: "success" or "error"dataType: Type of data returnedtotalSteps: Total step count (for steps)averageBPM: Average heart rate (for heartRate)workoutCount: Number of workouts (for workouts)averageSleepHours: Average sleep duration (for sleep)totalCalories: Total active energy (for activeEnergy)
Get current location, geocode addresses, search places, and calculate distances.
Name: accessLocation
Actions: current, geocode, reverse, search, distance
Requirements:
- Location Services capability
- User permission via
NSLocationWhenInUseUsageDescription
Arguments:
action: The operation to performaddress: Address to geocode (for geocode action)latitude: Latitude coordinatelongitude: Longitude coordinatelatitude2: Second latitude (for distance action)longitude2: Second longitude (for distance action)searchQuery: Search query for places (for search action)radius: Search radius in meters (default: 1000)
Example:
let locationTool = LocationTool()
// Get current location
let currentArgs = LocationTool.Arguments(action: "current")
let location = try await locationTool.call(arguments: currentArgs)
// Geocode an address
let geocodeArgs = LocationTool.Arguments(
action: "geocode",
address: "1 Apple Park Way, Cupertino, CA"
)
let coordinates = try await locationTool.call(arguments: geocodeArgs)
// Reverse geocode coordinates
let reverseArgs = LocationTool.Arguments(
action: "reverse",
latitude: 37.3349,
longitude: -122.0090
)
let address = try await locationTool.call(arguments: reverseArgs)
// Search for nearby places
let searchArgs = LocationTool.Arguments(
action: "search",
searchQuery: "coffee shop",
radius: 2000
)
let places = try await locationTool.call(arguments: searchArgs)
// Calculate distance between two points
let distanceArgs = LocationTool.Arguments(
action: "distance",
latitude: 37.3349,
longitude: -122.0090,
latitude2: 37.7749,
longitude2: -122.4194
)
let distance = try await locationTool.call(arguments: distanceArgs)Returns:
status: "success" or "error"latitude: Latitude coordinatelongitude: Longitude coordinateaddress: Formatted address stringdistanceMeters: Distance in meters (for distance action)bearing: Compass bearing (for distance action)direction: Cardinal direction (N, NE, E, etc.)
Control music playback and search the Apple Music catalog.
Name: controlMusic
Actions: search, play, pause, stop, skip, previous, nowPlaying
Requirements:
- MusicKit capability
- User permission via
NSAppleMusicUsageDescription
Arguments:
action: The operation to performquery: Search query (for search action)trackId: Track identifier (for play action)
Example:
let musicTool = MusicTool()
// Search for music
let searchArgs = MusicTool.Arguments(
action: "search",
query: "Bohemian Rhapsody"
)
let results = try await musicTool.call(arguments: searchArgs)
// Get now playing
let nowPlayingArgs = MusicTool.Arguments(action: "nowPlaying")
let currentTrack = try await musicTool.call(arguments: nowPlayingArgs)
// Control playback
let pauseArgs = MusicTool.Arguments(action: "pause")
try await musicTool.call(arguments: pauseArgs)Returns:
status: "success" or "error"results: Search results with track informationtitle: Track titleartist: Artist namealbum: Album name
Create, read, update, and complete reminders using EventKit.
Name: manageReminders
Actions: create, query, complete, update, delete
Requirements:
- Reminders entitlement
- User permission via
NSRemindersUsageDescription
Arguments:
action: The operation to performtitle: Reminder title (required for create)notes: Additional notesdueDate: Due date inYYYY-MM-DD HH:mm:ssformatpriority: Priority level (none,low,medium,high)listName: Reminder list name (optional)reminderId: Reminder identifier (for update/complete/delete)filter: Query filter (all,incomplete,completed,today,overdue)
Example:
let remindersTool = RemindersTool()
// Create a reminder
let createArgs = RemindersTool.Arguments(
action: "create",
title: "Buy groceries",
dueDate: "2025-11-15 18:00:00",
priority: "high",
notes: "Milk, bread, eggs"
)
let reminder = try await remindersTool.call(arguments: createArgs)
// Query incomplete reminders
let queryArgs = RemindersTool.Arguments(
action: "query",
filter: "incomplete"
)
let reminders = try await remindersTool.call(arguments: queryArgs)
// Complete a reminder
let completeArgs = RemindersTool.Arguments(
action: "complete",
reminderId: "reminder-id-here"
)
try await remindersTool.call(arguments: completeArgs)
// Update a reminder
let updateArgs = RemindersTool.Arguments(
action: "update",
reminderId: "reminder-id-here",
title: "Buy groceries and household items",
priority: "medium"
)
try await remindersTool.call(arguments: updateArgs)Returns:
status: "success" or "error"reminderId: Unique reminder identifiertitle: Reminder titlelist: List namedueDate: Formatted due datepriority: Priority level string
Fetch real-time weather information using the OpenMeteo API.
Name: getWeather
API: OpenMeteo (free, no API key required)
Arguments:
city: City name (e.g., "New York", "London", "Tokyo")
Example:
let weatherTool = WeatherTool()
// Get weather for a city
let arguments = WeatherTool.Arguments(city: "San Francisco")
let weather = try await weatherTool.call(arguments: arguments)
// Access weather data
let temperature = weather.value(Double.self, forProperty: "temperature")
let condition = weather.value(String.self, forProperty: "condition")
let humidity = weather.value(Double.self, forProperty: "humidity")
let windSpeed = weather.value(Double.self, forProperty: "windSpeed")
print("\(temperature)°C, \(condition)")
print("Humidity: \(humidity)%, Wind: \(windSpeed) km/h")Returns:
city: City nametemperature: Current temperature in Celsiuscondition: Weather condition descriptionhumidity: Humidity percentagewindSpeed: Wind speed in km/hfeelsLike: Feels-like temperature in Celsiuspressure: Atmospheric pressure in hPaprecipitation: Precipitation in mmunit: Temperature unit (always "Celsius")
Extract metadata from web pages using the LinkPresentation framework.
Name: getWebMetadata
Arguments:
url: Web page URL
Example:
let webMetadataTool = WebMetadataTool()
let arguments = WebMetadataTool.Arguments(
url: "https://www.example.com"
)
let metadata = try await webMetadataTool.call(arguments: arguments)
let title = metadata.value(String.self, forProperty: "title")
let description = metadata.value(String.self, forProperty: "description")Returns:
status: "success" or "error"url: Original URLtitle: Page titledescription: Page descriptionimageUrl: Preview image URL
Search the web using Exa's neural and keyword search capabilities.
Name: searchWeb
Requirements:
- Exa API key (sign up at https://exa.ai)
- API key stored in
@AppStorage("exaAPIKey")
Arguments:
query: Search querynumResults: Number of results to return (default: 10)searchType: Search mode (neuralorkeyword)
Example:
// First, configure the API key
@AppStorage("exaAPIKey") var exaAPIKey = "your-exa-api-key"
let webTool = WebTool()
// Neural search (semantic understanding)
let neuralArgs = WebTool.Arguments(
query: "latest advances in machine learning",
numResults: 5,
searchType: "neural"
)
let neuralResults = try await webTool.call(arguments: neuralArgs)
// Keyword search (traditional)
let keywordArgs = WebTool.Arguments(
query: "Swift 6 concurrency",
numResults: 10,
searchType: "keyword"
)
let keywordResults = try await webTool.call(arguments: keywordArgs)Returns:
status: "success" or "error"results: Array of search results with titles, URLs, and snippetscount: Number of results returned
FoundationModelsTools provides comprehensive token counting and context window management utilities for Transcript objects. These utilities help you prevent context overflow and manage long conversations effectively.
Estimation Methods:
estimatedTokenCount- Basic token counting using Apple's 4.5 characters per token ratiosafeEstimatedTokenCount- Conservative estimate with 25% buffer + 100 token overhead
Context Management:
isApproachingLimit(threshold:maxTokens:)- Check if approaching context limitsentriesWithinTokenBudget(_:)- Sliding window implementation for long conversations
import FoundationModels
import FoundationModelsTools
let transcript = Transcript([
.instructions("You are a helpful assistant"),
.prompt("What's the weather like?"),
.response("The weather is sunny and 72°F")
])
// Get token estimate
let tokens = transcript.estimatedTokenCount
print("Estimated tokens: \(tokens)")
// Get safe estimate with buffer
let safeTokens = transcript.safeEstimatedTokenCount
print("Safe estimate: \(safeTokens)")// Check if approaching limit (default: 70% of 4096 tokens)
if transcript.isApproachingLimit() {
print("Warning: Approaching context limit")
}
// Custom threshold and max tokens
if transcript.isApproachingLimit(threshold: 0.8, maxTokens: 8192) {
print("Using 80% threshold with 8K token limit")
}When conversations exceed token limits, use entriesWithinTokenBudget(_:) to maintain recent context:
let maxTokens = 2000
let trimmedEntries = transcript.entriesWithinTokenBudget(maxTokens)
let newTranscript = Transcript(trimmedEntries)
// The trimmed transcript:
// - Includes the first instructions entry if it fits within the budget
// - Includes as many recent entries as possible within budget
// - Preserves conversation recencyFor standalone text or structured content:
// Estimate tokens from text
let textTokens = estimateTokens(from: "Hello, world!")
print("Text tokens: \(textTokens)")
// Estimate tokens from GeneratedContent
let content = GeneratedContent(...)
let contentTokens = estimateTokens(from: content)
print("Content tokens: \(contentTokens)")- Use Safe Estimates: Always use
safeEstimatedTokenCountfor critical decisions to account for estimation variance - Set Conservative Thresholds: Default 70% threshold provides buffer for response generation
- Preserve Instructions: The sliding window attempts to keep the first system instructions entry for consistency, provided it fits within the token budget
- Monitor Long Conversations: Check token counts periodically in chat applications
import FoundationModels
import FoundationModelsTools
class ChatManager {
private var transcript = Transcript()
private let maxTokens = 4096
private let threshold = 0.7
init(systemInstructions: String) {
transcript = Transcript([.instructions(systemInstructions)])
}
func addMessage(_ message: String) async throws -> String {
// Add user message
transcript.append(.prompt(message))
// Check if we're approaching the limit
if transcript.isApproachingLimit(threshold: threshold, maxTokens: maxTokens) {
// Trim to fit budget (leaving room for response)
let budget = Int(Double(maxTokens) * 0.6) // Use 60% for history
let trimmedEntries = transcript.entriesWithinTokenBudget(budget)
transcript = Transcript(trimmedEntries)
print("Trimmed transcript to \(transcript.estimatedTokenCount) tokens")
}
// Generate response with managed context
let session = LanguageModelSession(model: .instant)
let response = try await session.generate(from: transcript)
transcript.append(.response(response))
return response.text
}
}Call tools directly without model integration:
import FoundationModelsTools
// Weather example
let weatherTool = WeatherTool()
let args = WeatherTool.Arguments(city: "Tokyo")
let result = try await weatherTool.call(arguments: args)
// Check for errors
if let error = result.value(String?.self, forProperty: "error") {
print("Error: \(error)")
} else {
let temp = result.value(Double.self, forProperty: "temperature")
print("Temperature: \(temp)°C")
}Use tools with Foundation Models:
import FoundationModels
import FoundationModelsTools
// Create model with tools
let model = LanguageModel.instant
let agent = Agent(
model: model,
tools: [
CalendarTool(),
LocationTool(),
WeatherTool(),
RemindersTool()
]
)
// The model can now use these tools
let response = try await agent.generate(
from: "What's the weather in New York and add a reminder to check it tomorrow?"
)
print(response.text)All tools return structured results with error information:
let calendarTool = CalendarTool()
do {
let result = try await calendarTool.call(
arguments: CalendarTool.Arguments(
action: "create",
title: "Meeting",
startDate: "invalid-date"
)
)
// Check status
let status = result.value(String.self, forProperty: "status")
if status == "error" {
let errorMessage = result.value(String.self, forProperty: "error")
print("Tool error: \(errorMessage)")
} else {
print("Success!")
}
} catch {
print("Exception: \(error.localizedDescription)")
}All tools that require system permissions handle authorization automatically:
- First Time: Tools will request permission when first called
- Denied: If denied, tools return an error with guidance
- Granted: Subsequent calls work without additional prompts
Example permission flow:
let locationTool = LocationTool()
// First call - requests permission
let result = try await locationTool.call(
arguments: LocationTool.Arguments(action: "current")
)
let status = result.value(String.self, forProperty: "status")
if status == "error" {
// Check if it's a permission error
let error = result.value(String.self, forProperty: "error")
if error.contains("permission") {
print("Please grant location permission in Settings")
}
}Tools use consistent date formats:
Calendar and Reminders:
- Format:
YYYY-MM-DD HH:mm:ss - Example:
2025-11-15 14:30:00 - 24-hour time format
Health Data:
- Format:
YYYY-MM-DD - Example:
2025-11-15 - Time component not required
Alternative formats supported by RemindersTool:
YYYY-MM-DD HH:mm(without seconds)MM/DD/YYYY HH:mm:ss(US format)- ISO 8601 format
All tools return GeneratedContent with a consistent structure:
let result = try await someTool.call(arguments: args)
// Access values with type safety
let stringValue = result.value(String.self, forProperty: "propertyName")
let intValue = result.value(Int.self, forProperty: "count")
let doubleValue = result.value(Double.self, forProperty: "temperature")
let boolValue = result.value(Bool.self, forProperty: "isCompleted")
// Optional values
let optionalString = result.value(String?.self, forProperty: "notes")Common properties across all tools:
status: Always "success" or "error"message: Human-readable messageerror: Error description (only present when status is "error")
Contributions are welcome! Please feel free to submit a Pull Request.