Skip to content

PR 1: LLM Provider Protocol & Types Foundation #2

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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