// ============================================================================= // HybridRecommendationService.swift // Hybrid recommendation service combining WASM engine with remote fallback // ============================================================================= import Foundation // MARK: - Hybrid Recommendation Service /// Actor-based service that combines local WASM recommendations with remote API fallback public actor HybridRecommendationService { private let wasmEngine: WasmRecommendationEngine private let stateManager: WasmStateManager private let healthKitIntegration: HealthKitVibeProvider? private let remoteClient: RemoteRecommendationClient? // Configuration private let minLocalRecommendations: Int private let enableRemoteFallback: Bool // Statistics private var localHits: Int = 0 private var remoteHits: Int = 0 /// Initialize the hybrid recommendation service public init( wasmPath: URL, embeddingDim: Int = 64, numActions: Int = 100, enableHealthKit: Bool = false, enableRemote: Bool = false, remoteBaseURL: URL? = nil, minLocalRecommendations: Int = 10 ) async throws { // Initialize WASM engine self.wasmEngine = try WasmRecommendationEngine( wasmPath: wasmPath, embeddingDim: embeddingDim, numActions: numActions ) // Initialize state manager self.stateManager = WasmStateManager() // Load persisted state if available if let savedState = try? await stateManager.loadState() { try? wasmEngine.loadState(savedState) } // Optional HealthKit integration for vibe detection self.healthKitIntegration = enableHealthKit ? HealthKitVibeProvider() : nil // Optional remote client self.remoteClient = enableRemote && remoteBaseURL != nil ? RemoteRecommendationClient(baseURL: remoteBaseURL!) : nil self.minLocalRecommendations = minLocalRecommendations self.enableRemoteFallback = enableRemote } // MARK: - Recommendations /// Get personalized recommendations public func getRecommendations( candidates: [UInt64], topK: Int = 10 ) async throws -> [ContentRecommendation] { // Get current vibe from HealthKit or use default let vibe = await getCurrentVibe() wasmEngine.setVibe(vibe) // Get local recommendations let localRecs = try await wasmEngine.recommend(candidates: candidates, topK: topK) localHits += 1 // If we have enough local recommendations, return them if localRecs.count >= minLocalRecommendations || !enableRemoteFallback { return localRecs.map { rec in ContentRecommendation( contentId: rec.contentId, score: rec.score, source: .local ) } } // Fallback to remote for additional recommendations var results = localRecs.map { rec in ContentRecommendation( contentId: rec.contentId, score: rec.score, source: .local ) } if let remote = remoteClient { let remainingCount = topK - localRecs.count if let remoteRecs = try? await remote.getRecommendations( vibe: vibe, count: remainingCount, exclude: Set(localRecs.map { $0.contentId }) ) { results.append(contentsOf: remoteRecs.map { rec in ContentRecommendation( contentId: rec.contentId, score: rec.score, source: .remote ) }) remoteHits += 1 } } return results } /// Get similar content public func getSimilar(to contentId: UInt64, topK: Int = 5) async throws -> [ContentRecommendation] { // This would typically use the embedding similarity // For now, use the recommendation system with the content as "context" let candidates = try await generateCandidates(excluding: contentId) return try await getRecommendations(candidates: candidates, topK: topK) } // MARK: - Learning /// Record a user interaction public func recordInteraction(_ interaction: UserInteraction) async { do { try await wasmEngine.learn(interaction: interaction) // Periodically save state if wasmEngine.updateCount % 50 == 0 { await saveState() } } catch { print("Failed to record interaction: \(error)") } } /// Record multiple interactions in batch public func recordInteractions(_ interactions: [UserInteraction]) async { for interaction in interactions { await recordInteraction(interaction) } } // MARK: - State Management /// Save current engine state public func saveState() async { do { let state = try wasmEngine.saveState() try await stateManager.saveState(state) } catch { print("Failed to save state: \(error)") } } /// Clear all learned data public func clearLearning() async { do { try await stateManager.clearState() // Reinitialize engine would be needed here } catch { print("Failed to clear learning: \(error)") } } // MARK: - Statistics /// Get service statistics public func getStatistics() -> ServiceStatistics { ServiceStatistics( localHits: localHits, remoteHits: remoteHits, explorationRate: wasmEngine.explorationRate, totalUpdates: wasmEngine.updateCount ) } // MARK: - Private Helpers private func getCurrentVibe() async -> VibeState { if let healthKit = healthKitIntegration { return await healthKit.getCurrentVibe() } return VibeState() // Default vibe } private func generateCandidates(excluding: UInt64) async throws -> [UInt64] { // In real implementation, this would query a content catalog // For now, return a sample set return (1...100).map { UInt64($0) }.filter { $0 != excluding } } } // MARK: - Supporting Types /// Recommendation with source information public struct ContentRecommendation { public let contentId: UInt64 public let score: Float public let source: RecommendationSource public enum RecommendationSource { case local case remote case hybrid } } /// Service statistics public struct ServiceStatistics { public let localHits: Int public let remoteHits: Int public let explorationRate: Float public let totalUpdates: UInt64 public var localHitRate: Float { let total = localHits + remoteHits return total > 0 ? Float(localHits) / Float(total) : 0 } } // MARK: - HealthKit Integration /// Provides vibe state from HealthKit data public actor HealthKitVibeProvider { public init() { // Request HealthKit permissions in real implementation } /// Get current vibe from HealthKit data public func getCurrentVibe() async -> VibeState { // In real implementation: // - Query HKHealthStore for heart rate, HRV, activity // - Compute energy level from activity data // - Estimate mood from HRV patterns // - Determine focus from recent activity // For now, return a simulated vibe based on time of day let hour = Calendar.current.component(.hour, from: Date()) let energy: Float let focus: Float let timeContext = Float(hour) / 24.0 switch hour { case 6..<9: // Morning energy = 0.6 focus = 0.7 case 9..<12: // Late morning energy = 0.8 focus = 0.9 case 12..<14: // Lunch energy = 0.5 focus = 0.4 case 14..<17: // Afternoon energy = 0.7 focus = 0.8 case 17..<20: // Evening energy = 0.6 focus = 0.5 case 20..<23: // Night energy = 0.4 focus = 0.3 default: // Late night energy = 0.2 focus = 0.2 } return VibeState( energy: energy, mood: 0.5, // Neutral focus: focus, timeContext: timeContext ) } } // MARK: - Remote Client /// Client for remote recommendation API public actor RemoteRecommendationClient { private let baseURL: URL private let session: URLSession public init(baseURL: URL) { self.baseURL = baseURL self.session = URLSession(configuration: .default) } /// Get recommendations from remote API public func getRecommendations( vibe: VibeState, count: Int, exclude: Set ) async throws -> [Recommendation] { // Build request var request = URLRequest(url: baseURL.appendingPathComponent("recommendations")) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let body: [String: Any] = [ "vibe": [ "energy": vibe.energy, "mood": vibe.mood, "focus": vibe.focus, "time_context": vibe.timeContext ], "count": count, "exclude": Array(exclude) ] request.httpBody = try JSONSerialization.data(withJSONObject: body) // Make request let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw RemoteClientError.requestFailed } // Parse response guard let json = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { throw RemoteClientError.invalidResponse } return json.compactMap { item in guard let id = item["id"] as? UInt64, let score = item["score"] as? Float else { return nil } return Recommendation(contentId: id, score: score) } } } public enum RemoteClientError: Error { case requestFailed case invalidResponse }