Skip to content

PR 2: Apple Intelligence Provider Implementation #3

Description

@gilmanb1

Overview

Implement the Apple Intelligence provider using the macOS 26 FoundationModels framework. This is the primary/default provider that requires zero external dependencies.

Dependencies

Files to Create/Modify

File Action Description
Sources/SortAI/Core/LLM/AppleIntelligenceProvider.swift Create Main provider implementation
Sources/SortAI/Core/LLM/GenerableTypes.swift Create @generable struct definitions
Tests/SortAITests/AppleIntelligenceTests.swift Create Unit tests

Implementation Details

1. @generable Types for Structured Output

import FoundationModels

@Generable
struct FileCategoryResponse {
    @Guide(description: "The category path like 'Documents / Financial / Reports'")
    var categoryPath: String
    
    @Guide(description: "Confidence from 0.0 to 1.0")
    var confidence: Double
    
    @Guide(description: "Brief explanation for the categorization")
    var rationale: String
    
    @Guide(description: "Relevant keywords extracted from the file")
    var keywords: [String]
}

@Generable
struct EntityExtractionResponse {
    @Guide(description: "List of extracted entities with types")
    var entities: [EntityItem]
}

@Generable
struct EntityItem {
    @Guide(description: "The entity text")
    var text: String
    
    @Guide(description: "Entity type: person, organization, location, date, keyword")
    var type: String
}

2. AppleIntelligenceProvider

import Foundation
import FoundationModels

@available(macOS 26.0, *)
actor AppleIntelligenceProvider: LLMCategorizationProvider {
    let identifier = "apple-intelligence"
    let priority = 1  // Highest priority (default)
    
    private var session: LanguageModelSession?
    private let escalationThreshold: Double
    
    var supportsModelSelection: Bool { false }
    var supportsTemperature: Bool { false }
    var supportsCustomPrompts: Bool { false }
    
    init(escalationThreshold: Double = 0.5) {
        self.escalationThreshold = escalationThreshold
    }
    
    func isAvailable() async -> Bool {
        return LanguageModelSession.isSupported
    }
    
    func categorize(signature: FileSignature) async throws -> CategorizationResult {
        let session = try await getOrCreateSession()
        let prompt = buildCategorizationPrompt(signature: signature)
        
        let response: FileCategoryResponse = try await session.respond(
            to: prompt,
            generating: FileCategoryResponse.self
        )
        
        return CategorizationResult(
            categoryPath: CategoryPath(path: response.categoryPath),
            confidence: response.confidence,
            rationale: response.rationale,
            extractedKeywords: response.keywords,
            provider: identifier,
            escalationThreshold: escalationThreshold
        )
    }
    
    private func getOrCreateSession() async throws -> LanguageModelSession {
        if let session = session { return session }
        let newSession = LanguageModelSession()
        self.session = newSession
        return newSession
    }
    
    private func buildCategorizationPrompt(signature: FileSignature) -> String {
        var prompt = """
        Categorize this file based on its characteristics:
        
        Filename: \(signature.url.lastPathComponent)
        Type: \(signature.kind.rawValue)
        Size: \(signature.size) bytes
        """
        
        if let content = signature.textContent {
            prompt += "\n\nContent preview:\n\(content.prefix(1500))"
        }
        
        if !signature.keywords.isEmpty {
            prompt += "\n\nExtracted keywords: \(signature.keywords.joined(separator: ", "))"
        }
        
        prompt += """
        
        Suggest the most appropriate category path (e.g., "Documents / Work / Reports")
        and explain your reasoning.
        """
        
        return prompt
    }
}

3. Fallback for Pre-macOS 26

/// Stub provider for systems without Apple Intelligence
final class AppleIntelligenceUnavailableProvider: LLMCategorizationProvider, Sendable {
    let identifier = "apple-intelligence"
    let priority = 1
    var supportsModelSelection: Bool { false }
    var supportsTemperature: Bool { false }
    var supportsCustomPrompts: Bool { false }
    
    func isAvailable() async -> Bool { false }
    
    func categorize(signature: FileSignature) async throws -> CategorizationResult {
        throw CategorizationError.providerUnavailable("Apple Intelligence requires macOS 26+")
    }
}

Acceptance Criteria

  • Provider implements LLMCategorizationProvider protocol
  • Uses @Generable for type-safe structured output
  • Gracefully handles macOS version check
  • Session is reused for performance
  • Prompts are well-structured for categorization
  • Confidence values are properly mapped
  • Escalation threshold is configurable
  • All tests pass on macOS 26+
  • Compilation succeeds on macOS 15 (with availability guards)

Testing

@available(macOS 26.0, *)
func testAppleIntelligenceAvailable() async {
    let provider = AppleIntelligenceProvider()
    let available = await provider.isAvailable()
    XCTAssertTrue(available, "Apple Intelligence should be available on macOS 26+")
}

@available(macOS 26.0, *)
func testAppleIntelligenceCategorization() async throws {
    let provider = AppleIntelligenceProvider()
    let signature = FileSignature.mock(
        filename: "quarterly_report_2025.pdf",
        content: "Q4 2025 Financial Summary..."
    )
    
    let result = try await provider.categorize(signature: signature)
    
    XCTAssertEqual(result.provider, "apple-intelligence")
    XCTAssertGreaterThan(result.confidence, 0.0)
    XCTAssertFalse(result.categoryPath.path.isEmpty)
}

Performance Expectations

Based on prototype testing:

  • Simple categorization: 1.5-1.9s
  • Entity extraction: 0.5-0.9s
  • Streaming responses: 1.5s initial

Estimated Size

~300 lines of code

Risk Assessment

Medium - New macOS 26 API. Mitigation: comprehensive @available guards and fallback stub.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestllm-providerLLM provider infrastructurephase-2Phase 2 - Parallel development

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions