Skip to content

PR 1: LLM Provider Protocol & Types Foundation #2

@gilmanb1

Description

@gilmanb1

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

  • Protocol compiles and is properly documented
  • All types conform to Sendable for actor safety
  • Result struct includes provider tracking field
  • Preference enum includes all four options
  • Error enum covers all failure modes
  • Unit tests verify type conformance

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    infrastructureCore infrastructure and protocolsllm-providerLLM provider infrastructurephase-1Phase 1 - Foundation

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions