Overview
Implement the UnifiedCategorizationService that orchestrates the provider cascade. This is the central service that manages provider selection, escalation, and fallback logic.
Dependencies
Files to Create
| File |
Action |
Description |
Sources/SortAI/Core/LLM/UnifiedCategorizationService.swift |
Create |
Main orchestration service |
Sources/SortAI/Core/LLM/ProviderSettingsAvailability.swift |
Create |
Settings availability struct |
Implementation Details
1. UnifiedCategorizationService
import Foundation
import Combine
/// Unified service that manages provider cascade
actor UnifiedCategorizationService {
// MARK: - Properties
private var providers: [any LLMCategorizationProvider] = []
private var currentProvider: (any LLMCategorizationProvider)?
private let ollamaInstaller = OllamaInstaller()
private var preference: ProviderPreference = .automatic
private var escalationThreshold: Double = 0.5
private var autoInstallOllama: Bool = true
// Observable state
@Published private(set) var activeProvider: String = "initializing"
@Published private(set) var isEscalating: Bool = false
// MARK: - Initialization
init() async {
await initialize()
}
func initialize() async {
providers = []
// Register Apple Intelligence (priority 1)
if #available(macOS 26.0, *) {
let appleProvider = AppleIntelligenceProvider(
escalationThreshold: escalationThreshold
)
if await appleProvider.isAvailable() {
providers.append(appleProvider)
}
}
// Register Ollama (priority 2)
let ollamaProvider = OllamaCategorizationProvider()
providers.append(ollamaProvider)
// Register Cloud providers (priority 3)
// OpenAICategorizationProvider, AnthropicProvider, etc.
// Register Local ML (priority 4 - always available fallback)
let localMLProvider = LocalMLProvider()
providers.append(localMLProvider)
// Sort by priority
providers.sort { $0.priority < $1.priority }
NSLog("📱 [UnifiedService] Initialized with %d providers", providers.count)
}
// MARK: - Configuration
func setPreference(_ pref: ProviderPreference) {
self.preference = pref
NSLog("📱 [UnifiedService] Preference: %@", pref.rawValue)
}
func setEscalationThreshold(_ threshold: Double) {
self.escalationThreshold = max(0.0, min(1.0, threshold))
}
func setAutoInstallOllama(_ enabled: Bool) {
self.autoInstallOllama = enabled
}
// MARK: - Categorization
func categorize(signature: FileSignature) async throws -> CategorizationResult {
let orderedProviders = getProvidersForPreference()
var lastError: Error?
var lastResult: CategorizationResult?
for provider in orderedProviders {
// Check availability
guard await provider.isAvailable() else {
NSLog("⚠️ [UnifiedService] Provider %@ unavailable", provider.identifier)
// Offer to install Ollama if needed
if provider.identifier == "ollama" && autoInstallOllama {
await handleOllamaUnavailable()
}
continue
}
do {
NSLog("🧠 [UnifiedService] Trying: %@", provider.identifier)
let result = try await provider.categorize(signature: signature)
currentProvider = provider
activeProvider = provider.identifier
// Check for escalation
if result.shouldEscalate && preference == .automatic {
NSLog("📈 [UnifiedService] Confidence %.2f < threshold, escalating...",
result.confidence)
isEscalating = true
lastResult = result
continue
}
isEscalating = false
return result
} catch {
NSLog("⚠️ [UnifiedService] %@ failed: %@",
provider.identifier, error.localizedDescription)
lastError = error
continue
}
}
// Return low-confidence result if we have one
if let result = lastResult {
isEscalating = false
return result
}
throw CategorizationError.allProvidersFailed(lastError)
}
// MARK: - Provider Ordering
private func getProvidersForPreference() -> [any LLMCategorizationProvider] {
switch preference {
case .automatic:
return providers.sorted { $0.priority < $1.priority }
case .appleIntelligenceOnly:
return providers.filter {
$0.identifier == "apple-intelligence" || $0.identifier == "local-ml"
}
case .preferOllama:
return providers.sorted { p1, p2 in
if p1.identifier == "ollama" { return true }
if p2.identifier == "ollama" { return false }
return p1.priority < p2.priority
}
case .cloud:
return providers.sorted { p1, p2 in
let cloudIds = ["openai", "anthropic"]
if cloudIds.contains(p1.identifier) { return true }
if cloudIds.contains(p2.identifier) { return false }
return p1.priority < p2.priority
}
}
}
// MARK: - Ollama Installation
private func handleOllamaUnavailable() async {
if !ollamaInstaller.isInstalled() {
let shouldInstall = await ollamaInstaller.showInstallationPrompt()
if shouldInstall {
do {
try await ollamaInstaller.install()
try await ollamaInstaller.pullModel("deepseek-r1:8b") { _ in }
NSLog("✅ [UnifiedService] Ollama installed")
} catch {
NSLog("❌ [UnifiedService] Install failed: %@", error.localizedDescription)
}
}
} else if await !ollamaInstaller.isServerRunning() {
do {
try await ollamaInstaller.launchOllama()
NSLog("✅ [UnifiedService] Ollama server started")
} catch {
NSLog("❌ [UnifiedService] Launch failed: %@", error.localizedDescription)
}
}
}
// MARK: - Settings Support
func getSettingsAvailability() -> ProviderSettingsAvailability {
guard let current = currentProvider else {
return ProviderSettingsAvailability.allEnabled
}
return ProviderSettingsAvailability(
modelSelection: current.supportsModelSelection,
temperature: current.supportsTemperature,
customPrompts: current.supportsCustomPrompts
)
}
func getAvailableProviders() async -> [String] {
var available: [String] = []
for provider in providers {
if await provider.isAvailable() {
available.append(provider.identifier)
}
}
return available
}
}
2. ProviderSettingsAvailability
struct ProviderSettingsAvailability: Sendable {
let modelSelection: Bool
let temperature: Bool
let customPrompts: Bool
static let allEnabled = ProviderSettingsAvailability(
modelSelection: true,
temperature: true,
customPrompts: true
)
static let allDisabled = ProviderSettingsAvailability(
modelSelection: false,
temperature: false,
customPrompts: false
)
}
Provider Cascade Behavior
Automatic Mode (Default)
1. Apple Intelligence → if available AND confidence >= threshold
2. Ollama → if available AND confidence >= threshold
3. Cloud → if configured AND confidence >= threshold
4. Local ML → always available, final fallback
Escalation Flow
Apple Intelligence returns confidence 0.4 (< 0.5 threshold)
↓
Mark shouldEscalate = true
↓
Try Ollama (if available)
↓
If Ollama unavailable → offer installation
↓
If all fail → return best result (even if low confidence)
Acceptance Criteria
Testing
func testAutomaticModeOrdering() async {
let service = await UnifiedCategorizationService()
await service.setPreference(.automatic)
// Verify Apple Intelligence is first when available
let providers = await service.getAvailableProviders()
if providers.contains("apple-intelligence") {
XCTAssertEqual(providers.first, "apple-intelligence")
}
}
func testPreferOllamaOrdering() async {
let service = await UnifiedCategorizationService()
await service.setPreference(.preferOllama)
// Categorize and verify Ollama was tried first
// (when available)
}
func testEscalationOnLowConfidence() async throws {
let service = await UnifiedCategorizationService()
await service.setPreference(.automatic)
await service.setEscalationThreshold(0.9) // High threshold
let signature = FileSignature.mock(filename: "ambiguous_file.dat")
let result = try await service.categorize(signature: signature)
// With high threshold, should have escalated
// Result may come from Ollama or later provider
}
func testFallbackOnProviderFailure() async throws {
let service = await UnifiedCategorizationService()
// Even if all LLMs fail, Local ML should work
let signature = FileSignature.mock(filename: "test.txt")
let result = try await service.categorize(signature: signature)
XCTAssertNotNil(result)
XCTAssertFalse(result.categoryPath.path.isEmpty)
}
Estimated Size
~250-300 lines of code
Risk Assessment
Medium - Core orchestration logic. Mitigation: comprehensive test coverage, gradual rollout with feature flag.
Overview
Implement the
UnifiedCategorizationServicethat orchestrates the provider cascade. This is the central service that manages provider selection, escalation, and fallback logic.Dependencies
Files to Create
Sources/SortAI/Core/LLM/UnifiedCategorizationService.swiftSources/SortAI/Core/LLM/ProviderSettingsAvailability.swiftImplementation Details
1. UnifiedCategorizationService
2. ProviderSettingsAvailability
Provider Cascade Behavior
Automatic Mode (Default)
Escalation Flow
Acceptance Criteria
Testing
Estimated Size
~250-300 lines of code
Risk Assessment
Medium - Core orchestration logic. Mitigation: comprehensive test coverage, gradual rollout with feature flag.