Overview
Create the foundational protocol and types that all LLM providers must implement. This is the base contract that enables the provider cascade architecture.
Dependencies
None - This is the foundation PR that all others depend on.
Files to Create/Modify
| File |
Action |
Description |
Sources/SortAI/Core/LLM/LLMCategorizationProvider.swift |
Create |
Protocol definition |
Sources/SortAI/Core/LLM/CategorizationResult.swift |
Create |
Result struct |
Sources/SortAI/Core/LLM/ProviderPreference.swift |
Create |
User preference enum |
Implementation Details
1. LLMCategorizationProvider Protocol
/// Protocol for all LLM providers in the categorization cascade
protocol LLMCategorizationProvider: Sendable {
/// Unique identifier for this provider
var identifier: String { get }
/// Priority in the cascade (lower = higher priority)
var priority: Int { get }
/// Whether this provider supports model selection
var supportsModelSelection: Bool { get }
/// Whether this provider supports temperature adjustment
var supportsTemperature: Bool { get }
/// Whether this provider supports custom prompts
var supportsCustomPrompts: Bool { get }
/// Check if provider is currently available
func isAvailable() async -> Bool
/// Categorize a file signature
func categorize(signature: FileSignature) async throws -> CategorizationResult
}
2. CategorizationResult Struct
/// Result from categorization with provider info
struct CategorizationResult: Sendable {
let categoryPath: CategoryPath
let confidence: Double
let rationale: String
let extractedKeywords: [String]
let provider: String
let shouldEscalate: Bool
/// Convenience initializer with default escalation logic
init(categoryPath: CategoryPath, confidence: Double, rationale: String,
extractedKeywords: [String], provider: String, escalationThreshold: Double = 0.5) {
self.categoryPath = categoryPath
self.confidence = confidence
self.rationale = rationale
self.extractedKeywords = extractedKeywords
self.provider = provider
self.shouldEscalate = confidence < escalationThreshold
}
}
3. ProviderPreference Enum
/// User preference for provider selection
enum ProviderPreference: String, Codable, CaseIterable, Sendable {
case automatic = "automatic"
case appleIntelligenceOnly = "apple-intelligence-only"
case preferOllama = "prefer-ollama"
case cloud = "cloud"
var displayName: String {
switch self {
case .automatic: return "Automatic (Recommended)"
case .appleIntelligenceOnly: return "Apple Intelligence Only"
case .preferOllama: return "Prefer Ollama"
case .cloud: return "Cloud (OpenAI/Anthropic)"
}
}
var description: String {
switch self {
case .automatic:
return "Uses Apple Intelligence, falls back to Ollama for complex files"
case .appleIntelligenceOnly:
return "Never uses external LLMs"
case .preferOllama:
return "Uses Ollama first, Apple Intelligence as fallback"
case .cloud:
return "Requires API key"
}
}
}
4. CategorizationError Enum
enum CategorizationError: LocalizedError {
case allProvidersFailed(Error?)
case providerUnavailable(String)
case invalidResponse
case timeout
var errorDescription: String? {
switch self {
case .allProvidersFailed(let underlying):
return "All AI providers failed. Last error: \(underlying?.localizedDescription ?? "Unknown")"
case .providerUnavailable(let name):
return "Provider '\(name)' is not available"
case .invalidResponse:
return "Provider returned an invalid response"
case .timeout:
return "Provider request timed out"
}
}
}
Acceptance Criteria
Testing
func testCategorizationResultInit() {
let result = CategorizationResult(
categoryPath: CategoryPath(path: "Documents / Work"),
confidence: 0.8,
rationale: "Test",
extractedKeywords: ["test"],
provider: "test-provider"
)
XCTAssertFalse(result.shouldEscalate) // 0.8 > 0.5 threshold
}
func testCategorizationResultEscalation() {
let result = CategorizationResult(
categoryPath: CategoryPath(path: "Documents"),
confidence: 0.4,
rationale: "Low confidence",
extractedKeywords: [],
provider: "test-provider"
)
XCTAssertTrue(result.shouldEscalate) // 0.4 < 0.5 threshold
}
Estimated Size
~150 lines of code
Risk Assessment
Low - Pure protocol and type definitions with no external dependencies.
Overview
Create the foundational protocol and types that all LLM providers must implement. This is the base contract that enables the provider cascade architecture.
Dependencies
None - This is the foundation PR that all others depend on.
Files to Create/Modify
Sources/SortAI/Core/LLM/LLMCategorizationProvider.swiftSources/SortAI/Core/LLM/CategorizationResult.swiftSources/SortAI/Core/LLM/ProviderPreference.swiftImplementation Details
1. LLMCategorizationProvider Protocol
2. CategorizationResult Struct
3. ProviderPreference Enum
4. CategorizationError Enum
Acceptance Criteria
Sendablefor actor safetyTesting
Estimated Size
~150 lines of code
Risk Assessment
Low - Pure protocol and type definitions with no external dependencies.