Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,347 @@
// =============================================================================
// 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<UInt64>
) 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
}

View File

@@ -0,0 +1,40 @@
// swift-tools-version:5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "RuvectorRecommendation",
platforms: [
.iOS(.v16),
.macOS(.v13)
],
products: [
.library(
name: "RuvectorRecommendation",
targets: ["RuvectorRecommendation"]),
],
dependencies: [
// WasmKit for WASM runtime
.package(url: "https://github.com/swiftwasm/WasmKit.git", from: "0.1.0"),
],
targets: [
.target(
name: "RuvectorRecommendation",
dependencies: [
.product(name: "WasmKit", package: "WasmKit"),
],
path: ".",
exclude: ["Package.swift", "Resources"],
sources: ["WasmRecommendationEngine.swift", "HybridRecommendationService.swift"],
resources: [
.copy("Resources/recommendation.wasm")
]
),
.testTarget(
name: "RuvectorRecommendationTests",
dependencies: ["RuvectorRecommendation"],
path: "Tests"
),
]
)

Binary file not shown.

View File

@@ -0,0 +1,637 @@
//
// RuvectorWasm.swift
// Privacy-Preserving On-Device AI for iOS
//
// Uses WasmKit to run Ruvector WASM directly on iOS
// Minimum iOS: 15.0 (WasmKit requirement)
//
import Foundation
// MARK: - Core Types
/// Distance metric for vector similarity
public enum DistanceMetric: UInt8 {
case euclidean = 0
case cosine = 1
case manhattan = 2
case dotProduct = 3
}
/// Quantization mode for memory optimization
public enum QuantizationMode: UInt8 {
case none = 0
case scalar = 1 // 4x compression
case binary = 2 // 32x compression
case product = 3 // Variable compression
}
/// Search result with vector ID and distance
public struct SearchResult: Identifiable {
public let id: UInt64
public let distance: Float
}
// MARK: - Health Learning Types
/// Health metric types (privacy-preserving)
public enum HealthMetricType: UInt8 {
case heartRate = 0
case steps = 1
case sleep = 2
case activeEnergy = 3
case exerciseMinutes = 4
case standHours = 5
case distance = 6
case flightsClimbed = 7
case mindfulness = 8
case respiratoryRate = 9
case bloodOxygen = 10
case hrv = 11
}
/// Health state for learning (no actual values stored)
public struct HealthState {
public let metric: HealthMetricType
public let valueBucket: UInt8 // 0-9 normalized
public let hour: UInt8
public let dayOfWeek: UInt8
public init(metric: HealthMetricType, valueBucket: UInt8, hour: UInt8, dayOfWeek: UInt8) {
self.metric = metric
self.valueBucket = min(valueBucket, 9)
self.hour = min(hour, 23)
self.dayOfWeek = min(dayOfWeek, 6)
}
}
// MARK: - Location Learning Types
/// Location categories (no coordinates stored)
public enum LocationCategory: UInt8 {
case home = 0
case work = 1
case gym = 2
case dining = 3
case shopping = 4
case transit = 5
case outdoor = 6
case entertainment = 7
case healthcare = 8
case education = 9
case unknown = 10
}
/// Location state for learning
public struct LocationState {
public let category: LocationCategory
public let hour: UInt8
public let dayOfWeek: UInt8
public let durationMinutes: UInt16
public init(category: LocationCategory, hour: UInt8, dayOfWeek: UInt8, durationMinutes: UInt16) {
self.category = category
self.hour = min(hour, 23)
self.dayOfWeek = min(dayOfWeek, 6)
self.durationMinutes = durationMinutes
}
}
// MARK: - Communication Learning Types
/// Communication event types
public enum CommEventType: UInt8 {
case callIncoming = 0
case callOutgoing = 1
case messageReceived = 2
case messageSent = 3
case emailReceived = 4
case emailSent = 5
case notification = 6
}
/// Communication state
public struct CommState {
public let eventType: CommEventType
public let hour: UInt8
public let dayOfWeek: UInt8
public let responseTimeBucket: UInt8 // 0-9 normalized
public init(eventType: CommEventType, hour: UInt8, dayOfWeek: UInt8, responseTimeBucket: UInt8) {
self.eventType = eventType
self.hour = min(hour, 23)
self.dayOfWeek = min(dayOfWeek, 6)
self.responseTimeBucket = min(responseTimeBucket, 9)
}
}
// MARK: - Calendar Learning Types
/// Calendar event types
public enum CalendarEventType: UInt8 {
case meeting = 0
case focusTime = 1
case personal = 2
case travel = 3
case breakTime = 4
case exercise = 5
case social = 6
case deadline = 7
}
/// Calendar event for learning
public struct CalendarEvent {
public let eventType: CalendarEventType
public let startHour: UInt8
public let durationMinutes: UInt16
public let dayOfWeek: UInt8
public let isRecurring: Bool
public let hasAttendees: Bool
public init(eventType: CalendarEventType, startHour: UInt8, durationMinutes: UInt16,
dayOfWeek: UInt8, isRecurring: Bool, hasAttendees: Bool) {
self.eventType = eventType
self.startHour = min(startHour, 23)
self.durationMinutes = durationMinutes
self.dayOfWeek = min(dayOfWeek, 6)
self.isRecurring = isRecurring
self.hasAttendees = hasAttendees
}
}
/// Time slot pattern
public struct TimeSlotPattern {
public let busyProbability: Float
public let avgMeetingDuration: Float
public let focusScore: Float
public let eventCount: UInt32
}
/// Focus time suggestion
public struct FocusTimeSuggestion: Identifiable {
public var id: String { "\(day)-\(startHour)" }
public let day: UInt8
public let startHour: UInt8
public let score: Float
}
// MARK: - App Usage Learning Types
/// App categories
public enum AppCategory: UInt8 {
case social = 0
case productivity = 1
case entertainment = 2
case news = 3
case communication = 4
case health = 5
case navigation = 6
case shopping = 7
case gaming = 8
case education = 9
case finance = 10
case utilities = 11
}
/// App usage session
public struct AppUsageSession {
public let category: AppCategory
public let durationSeconds: UInt32
public let hour: UInt8
public let dayOfWeek: UInt8
public let isActiveUse: Bool
public init(category: AppCategory, durationSeconds: UInt32, hour: UInt8,
dayOfWeek: UInt8, isActiveUse: Bool) {
self.category = category
self.durationSeconds = durationSeconds
self.hour = min(hour, 23)
self.dayOfWeek = min(dayOfWeek, 6)
self.isActiveUse = isActiveUse
}
}
/// Screen time summary
public struct ScreenTimeSummary {
public let totalMinutes: Float
public let topCategory: AppCategory
public let byCategory: [AppCategory: Float]
}
/// Wellbeing insight
public struct WellbeingInsight: Identifiable {
public var id: String { category }
public let category: String
public let message: String
public let score: Float
}
// MARK: - iOS Context & Recommendations
/// Device context for recommendations
public struct IOSContext {
public let hour: UInt8
public let dayOfWeek: UInt8
public let isWeekend: Bool
public let batteryLevel: UInt8 // 0-100
public let networkType: UInt8 // 0=none, 1=wifi, 2=cellular
public let locationCategory: LocationCategory
public let recentAppCategory: AppCategory
public let activityLevel: UInt8 // 0-10
public let healthScore: Float // 0-1
public init(hour: UInt8, dayOfWeek: UInt8, batteryLevel: UInt8 = 100,
networkType: UInt8 = 1, locationCategory: LocationCategory = .unknown,
recentAppCategory: AppCategory = .utilities, activityLevel: UInt8 = 5,
healthScore: Float = 0.5) {
self.hour = min(hour, 23)
self.dayOfWeek = min(dayOfWeek, 6)
self.isWeekend = dayOfWeek == 0 || dayOfWeek == 6
self.batteryLevel = min(batteryLevel, 100)
self.networkType = min(networkType, 2)
self.locationCategory = locationCategory
self.recentAppCategory = recentAppCategory
self.activityLevel = min(activityLevel, 10)
self.healthScore = min(max(healthScore, 0), 1)
}
}
/// Activity suggestion
public struct ActivitySuggestion: Identifiable {
public var id: String { category }
public let category: String
public let confidence: Float
public let reason: String
}
/// Context-aware recommendations
public struct ContextRecommendations {
public let suggestedAppCategory: AppCategory
public let focusScore: Float
public let activitySuggestions: [ActivitySuggestion]
public let optimalNotificationTime: Bool
}
// MARK: - WASM Runtime Error
/// WASM runtime errors
public enum RuvectorError: Error, LocalizedError {
case wasmNotLoaded
case initializationFailed(String)
case memoryAllocationFailed
case invalidInput(String)
case serializationFailed
case deserializationFailed
public var errorDescription: String? {
switch self {
case .wasmNotLoaded:
return "WASM module not loaded"
case .initializationFailed(let msg):
return "Initialization failed: \(msg)"
case .memoryAllocationFailed:
return "Memory allocation failed"
case .invalidInput(let msg):
return "Invalid input: \(msg)"
case .serializationFailed:
return "Serialization failed"
case .deserializationFailed:
return "Deserialization failed"
}
}
}
// MARK: - Ruvector WASM Runtime
/// Main entry point for Ruvector WASM on iOS
/// Uses WasmKit for native WASM execution
public final class RuvectorWasm {
/// Shared instance (singleton pattern for resource efficiency)
public static let shared = RuvectorWasm()
// WASM runtime state
private var isLoaded = false
private var wasmBytes: Data?
private var memoryPtr: UnsafeMutableRawPointer?
private var memorySize: Int = 0
// Learning state handles
private var healthLearnerHandle: Int32 = -1
private var locationLearnerHandle: Int32 = -1
private var commLearnerHandle: Int32 = -1
private var calendarLearnerHandle: Int32 = -1
private var appUsageLearnerHandle: Int32 = -1
private var iosLearnerHandle: Int32 = -1
private init() {}
// MARK: - Initialization
/// Load WASM module from bundle
/// - Parameter bundlePath: Path to .wasm file in app bundle
public func load(from bundlePath: String) throws {
guard let data = FileManager.default.contents(atPath: bundlePath) else {
throw RuvectorError.initializationFailed("WASM file not found at \(bundlePath)")
}
try load(wasmData: data)
}
/// Load WASM module from data
/// - Parameter wasmData: Raw WASM bytes
public func load(wasmData: Data) throws {
self.wasmBytes = wasmData
// In production: Initialize WasmKit runtime here
// For now, mark as loaded for API design
// TODO: Integrate WasmKit when added as dependency
//
// Example WasmKit integration:
// let module = try WasmKit.Module(bytes: [UInt8](wasmData))
// let instance = try module.instantiate()
// self.wasmInstance = instance
isLoaded = true
}
/// Check if WASM is loaded
public var isReady: Bool { isLoaded }
// MARK: - Memory Management
/// Allocate memory in WASM linear memory
private func allocate(size: Int) throws -> Int {
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
// TODO: Call wasm_alloc export
return 0
}
/// Free memory in WASM linear memory
private func free(ptr: Int, size: Int) throws {
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
// TODO: Call wasm_free export
}
// MARK: - SIMD Operations
/// Compute dot product of two vectors
public func dotProduct(_ a: [Float], _ b: [Float]) throws -> Float {
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
guard a.count == b.count else {
throw RuvectorError.invalidInput("Vectors must have same length")
}
// Pure Swift fallback (SIMD when available)
var result: Float = 0
for i in 0..<a.count {
result += a[i] * b[i]
}
return result
}
/// Compute L2 distance
public func l2Distance(_ a: [Float], _ b: [Float]) throws -> Float {
guard a.count == b.count else {
throw RuvectorError.invalidInput("Vectors must have same length")
}
var sum: Float = 0
for i in 0..<a.count {
let diff = a[i] - b[i]
sum += diff * diff
}
return sqrt(sum)
}
/// Compute cosine similarity
public func cosineSimilarity(_ a: [Float], _ b: [Float]) throws -> Float {
guard a.count == b.count else {
throw RuvectorError.invalidInput("Vectors must have same length")
}
var dot: Float = 0
var normA: Float = 0
var normB: Float = 0
for i in 0..<a.count {
dot += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
}
let denom = sqrt(normA) * sqrt(normB)
return denom > 0 ? dot / denom : 0
}
// MARK: - iOS Learner (Unified)
/// Initialize unified iOS learner
public func initIOSLearner() throws {
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
// TODO: Call ios_learner_init export
iosLearnerHandle = 0
}
/// Update health metrics
public func updateHealth(_ state: HealthState) throws {
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
// TODO: Call ios_update_health export
}
/// Update location
public func updateLocation(_ state: LocationState) throws {
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
// TODO: Call ios_update_location export
}
/// Update communication patterns
public func updateCommunication(_ state: CommState) throws {
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
// TODO: Call ios_update_communication export
}
/// Update calendar
public func updateCalendar(_ event: CalendarEvent) throws {
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
// TODO: Call ios_update_calendar export
}
/// Update app usage
public func updateAppUsage(_ session: AppUsageSession) throws {
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
// TODO: Call ios_update_app_usage export
}
/// Get context-aware recommendations
public func getRecommendations(_ context: IOSContext) throws -> ContextRecommendations {
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
// TODO: Call ios_get_recommendations export
// For now, return sensible defaults
return ContextRecommendations(
suggestedAppCategory: .productivity,
focusScore: 0.7,
activitySuggestions: [
ActivitySuggestion(category: "Focus", confidence: 0.8, reason: "Good time for deep work")
],
optimalNotificationTime: context.hour >= 9 && context.hour <= 18
)
}
/// Train one iteration (call periodically)
public func trainIteration() throws {
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
// TODO: Call ios_train export
}
// MARK: - Calendar Learning
/// Initialize calendar learner
public func initCalendarLearner() throws {
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
// TODO: Call calendar_init export
calendarLearnerHandle = 0
}
/// Learn from calendar event
public func learnCalendarEvent(_ event: CalendarEvent) throws {
guard calendarLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
// TODO: Call calendar_learn_event export
}
/// Get busy probability for time slot
public func calendarBusyProbability(hour: UInt8, dayOfWeek: UInt8) throws -> Float {
guard calendarLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
// TODO: Call calendar_is_busy export
return 0.5
}
/// Suggest focus times
public func suggestFocusTimes(durationHours: UInt8) throws -> [FocusTimeSuggestion] {
guard calendarLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
// TODO: Call through WASM
return [
FocusTimeSuggestion(day: 1, startHour: 9, score: 0.9),
FocusTimeSuggestion(day: 2, startHour: 14, score: 0.85)
]
}
// MARK: - App Usage Learning
/// Initialize app usage learner
public func initAppUsageLearner() throws {
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
// TODO: Call app_usage_init export
appUsageLearnerHandle = 0
}
/// Learn from app session
public func learnAppSession(_ session: AppUsageSession) throws {
guard appUsageLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
// TODO: Call app_usage_learn export
}
/// Get screen time (hours)
public func screenTime() throws -> Float {
guard appUsageLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
// TODO: Call app_usage_screen_time export
return 2.5
}
// MARK: - Persistence
/// Serialize all learning state
public func serialize() throws -> Data {
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
// TODO: Call serialize exports for each learner
return Data()
}
/// Deserialize learning state
public func deserialize(_ data: Data) throws {
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
// TODO: Call deserialize exports
}
/// Save state to file
public func save(to url: URL) throws {
let data = try serialize()
try data.write(to: url)
}
/// Load state from file
public func restore(from url: URL) throws {
let data = try Data(contentsOf: url)
try deserialize(data)
}
}
// MARK: - SwiftUI Integration
#if canImport(SwiftUI)
import SwiftUI
/// Observable wrapper for SwiftUI
@available(iOS 15.0, macOS 12.0, *)
@MainActor
public final class RuvectorViewModel: ObservableObject {
@Published public private(set) var isReady = false
@Published public private(set) var recommendations: ContextRecommendations?
@Published public private(set) var screenTimeHours: Float = 0
@Published public private(set) var focusScore: Float = 0
private let runtime = RuvectorWasm.shared
public init() {}
/// Load WASM module
public func load(from bundlePath: String) async throws {
try runtime.load(from: bundlePath)
try runtime.initIOSLearner()
try runtime.initCalendarLearner()
try runtime.initAppUsageLearner()
isReady = true
}
/// Update recommendations for current context
public func updateRecommendations(context: IOSContext) async throws {
recommendations = try runtime.getRecommendations(context)
focusScore = recommendations?.focusScore ?? 0
}
/// Update screen time
public func updateScreenTime() async throws {
screenTimeHours = try runtime.screenTime()
}
/// Record app usage
public func recordAppUsage(_ session: AppUsageSession) async throws {
try runtime.learnAppSession(session)
try await updateScreenTime()
}
/// Record calendar event
public func recordCalendarEvent(_ event: CalendarEvent) async throws {
try runtime.learnCalendarEvent(event)
}
}
#endif
// MARK: - Combine Integration
#if canImport(Combine)
import Combine
@available(iOS 13.0, macOS 10.15, *)
extension RuvectorWasm {
/// Publisher for periodic training
public func trainingPublisher(interval: TimeInterval = 60) -> AnyPublisher<Void, Never> {
Timer.publish(every: interval, on: .main, in: .common)
.autoconnect()
.map { [weak self] _ in
try? self?.trainIteration()
}
.eraseToAnyPublisher()
}
}
#endif

View File

@@ -0,0 +1,180 @@
// =============================================================================
// RecommendationTests.swift
// Unit tests for the WASM recommendation engine
// =============================================================================
import XCTest
@testable import RuvectorRecommendation
final class RecommendationTests: XCTestCase {
// MARK: - Content Metadata Tests
func testContentMetadataCreation() {
let content = ContentMetadata(
id: 123,
contentType: .video,
durationSecs: 120,
categoryFlags: 0b1010,
popularity: 0.8,
recency: 0.9
)
XCTAssertEqual(content.id, 123)
XCTAssertEqual(content.contentType, .video)
XCTAssertEqual(content.durationSecs, 120)
}
func testContentMetadataFromDictionary() {
let dict: [String: Any] = [
"id": UInt64(456),
"type": UInt8(1),
"duration": UInt32(300),
"popularity": Float(0.7)
]
let content = ContentMetadata(from: dict)
XCTAssertNotNil(content)
XCTAssertEqual(content?.id, 456)
XCTAssertEqual(content?.contentType, .audio)
}
// MARK: - Vibe State Tests
func testVibeStateDefault() {
let vibe = VibeState()
XCTAssertEqual(vibe.energy, 0.5)
XCTAssertEqual(vibe.mood, 0.0)
XCTAssertEqual(vibe.focus, 0.5)
}
func testVibeStateCustom() {
let vibe = VibeState(
energy: 0.8,
mood: 0.5,
focus: 0.9,
timeContext: 0.3,
preferences: (0.1, 0.2, 0.3, 0.4)
)
XCTAssertEqual(vibe.energy, 0.8)
XCTAssertEqual(vibe.mood, 0.5)
XCTAssertEqual(vibe.preferences.0, 0.1)
}
// MARK: - Interaction Tests
func testUserInteraction() {
let interaction = UserInteraction(
contentId: 789,
interaction: .like,
timeSpent: 45.0,
position: 2
)
XCTAssertEqual(interaction.contentId, 789)
XCTAssertEqual(interaction.interaction, .like)
XCTAssertEqual(interaction.timeSpent, 45.0)
}
func testInteractionTypes() {
XCTAssertEqual(InteractionType.view.rawValue, 0)
XCTAssertEqual(InteractionType.like.rawValue, 1)
XCTAssertEqual(InteractionType.share.rawValue, 2)
XCTAssertEqual(InteractionType.skip.rawValue, 3)
XCTAssertEqual(InteractionType.complete.rawValue, 4)
XCTAssertEqual(InteractionType.dismiss.rawValue, 5)
}
// MARK: - Performance Tests
func testRecommendationSpeed() async throws {
// This test requires the actual WASM module to be available
// Skip if not in a full integration environment
// Performance baseline: should complete in under 100ms
let start = Date()
// Simulate recommendation workload
var total: Float = 0
for i in 0..<1000 {
total += Float(i) * 0.001
}
let duration = Date().timeIntervalSince(start)
XCTAssertLessThan(duration, 0.1, "Simulation should complete in under 100ms")
// Prevent optimization
XCTAssertGreaterThan(total, 0)
}
// MARK: - State Manager Tests
func testStateManagerSaveLoad() async throws {
let tempURL = FileManager.default.temporaryDirectory
.appendingPathComponent("test_state_\(UUID().uuidString).bin")
let manager = WasmStateManager(stateURL: tempURL)
// Save test data
let testData = Data([0x01, 0x02, 0x03, 0x04])
try await manager.saveState(testData)
// Load and verify
let loaded = try await manager.loadState()
XCTAssertEqual(loaded, testData)
// Cleanup
try await manager.clearState()
let afterClear = try await manager.loadState()
XCTAssertNil(afterClear)
}
// MARK: - Error Tests
func testWasmEngineErrors() {
let errors: [WasmEngineError] = [
.initializationFailed,
.functionNotFound("test"),
.embeddingFailed,
.saveFailed,
.loadFailed,
.invalidInput("test message")
]
for error in errors {
XCTAssertNotNil(error.errorDescription)
XCTAssertFalse(error.errorDescription!.isEmpty)
}
}
}
// MARK: - Statistics Tests
final class StatisticsTests: XCTestCase {
func testServiceStatistics() {
let stats = ServiceStatistics(
localHits: 80,
remoteHits: 20,
explorationRate: 0.1,
totalUpdates: 1000
)
XCTAssertEqual(stats.localHits, 80)
XCTAssertEqual(stats.remoteHits, 20)
XCTAssertEqual(stats.localHitRate, 0.8, accuracy: 0.01)
}
func testLocalHitRateZero() {
let stats = ServiceStatistics(
localHits: 0,
remoteHits: 0,
explorationRate: 0.1,
totalUpdates: 0
)
XCTAssertEqual(stats.localHitRate, 0)
}
}

View File

@@ -0,0 +1,434 @@
// =============================================================================
// WasmRecommendationEngine.swift
// High-performance WASM-based recommendation engine for iOS
// Compatible with WasmKit runtime
// =============================================================================
import Foundation
// MARK: - Types
/// Content metadata for embedding generation
public struct ContentMetadata {
public let id: UInt64
public let contentType: ContentType
public let durationSecs: UInt32
public let categoryFlags: UInt32
public let popularity: Float
public let recency: Float
public enum ContentType: UInt8 {
case video = 0
case audio = 1
case image = 2
case text = 3
}
public init(
id: UInt64,
contentType: ContentType,
durationSecs: UInt32 = 0,
categoryFlags: UInt32 = 0,
popularity: Float = 0.5,
recency: Float = 0.5
) {
self.id = id
self.contentType = contentType
self.durationSecs = durationSecs
self.categoryFlags = categoryFlags
self.popularity = popularity
self.recency = recency
}
}
/// User vibe/preference state
public struct VibeState {
public var energy: Float // 0.0 = calm, 1.0 = energetic
public var mood: Float // -1.0 = negative, 1.0 = positive
public var focus: Float // 0.0 = relaxed, 1.0 = focused
public var timeContext: Float // 0.0 = morning, 1.0 = night
public var preferences: (Float, Float, Float, Float)
public init(
energy: Float = 0.5,
mood: Float = 0.0,
focus: Float = 0.5,
timeContext: Float = 0.5,
preferences: (Float, Float, Float, Float) = (0, 0, 0, 0)
) {
self.energy = energy
self.mood = mood
self.focus = focus
self.timeContext = timeContext
self.preferences = preferences
}
}
/// User interaction types
public enum InteractionType: UInt8 {
case view = 0
case like = 1
case share = 2
case skip = 3
case complete = 4
case dismiss = 5
}
/// User interaction event
public struct UserInteraction {
public let contentId: UInt64
public let interaction: InteractionType
public let timeSpent: Float
public let position: UInt8
public init(
contentId: UInt64,
interaction: InteractionType,
timeSpent: Float = 0,
position: UInt8 = 0
) {
self.contentId = contentId
self.interaction = interaction
self.timeSpent = timeSpent
self.position = position
}
}
/// Recommendation result
public struct Recommendation {
public let contentId: UInt64
public let score: Float
}
// MARK: - WasmRecommendationEngine
/// High-performance recommendation engine powered by WebAssembly
///
/// Usage:
/// ```swift
/// let engine = try WasmRecommendationEngine(wasmPath: Bundle.main.url(forResource: "recommendation", withExtension: "wasm")!)
/// engine.setVibe(VibeState(energy: 0.8, mood: 0.5))
/// let recs = try engine.recommend(candidates: [1, 2, 3, 4, 5], topK: 3)
/// ```
public class WasmRecommendationEngine {
// MARK: - WASM Function References
// These would be populated by WasmKit's module instantiation
private let wasmModule: Any // WasmKit.Module
private let wasmInstance: Any // WasmKit.Instance
// Function pointers (simulated for demonstration)
private var initFunc: ((UInt32, UInt32) -> Int32)?
private var embedContentFunc: ((UInt64, UInt8, UInt32, UInt32, Float, Float) -> UnsafePointer<Float>?)?
private var setVibeFunc: ((Float, Float, Float, Float, Float, Float, Float, Float) -> Void)?
private var getRecommendationsFunc: ((UnsafePointer<UInt64>, UInt32, UInt32, UnsafeMutablePointer<UInt8>) -> UInt32)?
private var updateLearningFunc: ((UInt64, UInt8, Float, UInt8) -> Void)?
private var computeSimilarityFunc: ((UInt64, UInt64) -> Float)?
private var saveStateFunc: (() -> UInt32)?
private var loadStateFunc: ((UnsafePointer<UInt8>, UInt32) -> Int32)?
private var getEmbeddingDimFunc: (() -> UInt32)?
private var getExplorationRateFunc: (() -> Float)?
private var getUpdateCountFunc: (() -> UInt64)?
private let embeddingDim: Int
private let numActions: Int
// MARK: - Initialization
/// Initialize the recommendation engine with a WASM module
/// - Parameters:
/// - wasmPath: URL to the recommendation.wasm file
/// - embeddingDim: Embedding dimension (default: 64)
/// - numActions: Number of action slots (default: 100)
public init(
wasmPath: URL,
embeddingDim: Int = 64,
numActions: Int = 100
) throws {
self.embeddingDim = embeddingDim
self.numActions = numActions
// Load WASM module
// In real implementation, use WasmKit:
// let runtime = Runtime()
// let wasmData = try Data(contentsOf: wasmPath)
// module = try Module(bytes: Array(wasmData))
// instance = try module.instantiate(runtime: runtime)
self.wasmModule = NSNull() // Placeholder
self.wasmInstance = NSNull() // Placeholder
// Bind exported functions
try bindExports()
// Initialize engine
let result = initFunc?(UInt32(embeddingDim), UInt32(numActions)) ?? -1
guard result == 0 else {
throw WasmEngineError.initializationFailed
}
}
/// Bind WASM exported functions
private func bindExports() throws {
// In real implementation with WasmKit:
// initFunc = instance.exports["init"] as? Function
// embedContentFunc = instance.exports["embed_content"] as? Function
// ... etc
// For demonstration, these would be populated from the WASM instance
}
// MARK: - Content Embedding
/// Generate embedding for content
/// - Parameter content: Content metadata
/// - Returns: Embedding vector as Float array
public func embed(content: ContentMetadata) async throws -> [Float] {
guard let embedFunc = embedContentFunc else {
throw WasmEngineError.functionNotFound("embed_content")
}
guard let ptr = embedFunc(
content.id,
content.contentType.rawValue,
content.durationSecs,
content.categoryFlags,
content.popularity,
content.recency
) else {
throw WasmEngineError.embeddingFailed
}
// Copy embedding from WASM memory
let buffer = UnsafeBufferPointer(start: ptr, count: embeddingDim)
return Array(buffer)
}
// MARK: - Vibe State
/// Set the current user vibe state
/// - Parameter vibe: User's current vibe/mood state
public func setVibe(_ vibe: VibeState) {
setVibeFunc?(
vibe.energy,
vibe.mood,
vibe.focus,
vibe.timeContext,
vibe.preferences.0,
vibe.preferences.1,
vibe.preferences.2,
vibe.preferences.3
)
}
// MARK: - Recommendations
/// Get recommendations based on current vibe and history
/// - Parameters:
/// - candidates: Array of candidate content IDs
/// - topK: Number of recommendations to return
/// - Returns: Array of recommendations sorted by score
public func recommend(
candidates: [UInt64],
topK: Int = 10
) async throws -> [Recommendation] {
guard let getRecsFunc = getRecommendationsFunc else {
throw WasmEngineError.functionNotFound("get_recommendations")
}
// Prepare output buffer (12 bytes per recommendation: 8 for ID + 4 for score)
let outputSize = topK * 12
var outputBuffer = [UInt8](repeating: 0, count: outputSize)
let count = candidates.withUnsafeBufferPointer { candidatesPtr in
outputBuffer.withUnsafeMutableBufferPointer { outputPtr in
getRecsFunc(
candidatesPtr.baseAddress!,
UInt32(candidates.count),
UInt32(topK),
outputPtr.baseAddress!
)
}
}
// Parse results
var recommendations: [Recommendation] = []
for i in 0..<Int(count) {
let offset = i * 12
// Extract ID (8 bytes, little-endian)
let id = outputBuffer[offset..<offset+8].withUnsafeBytes { ptr in
ptr.load(as: UInt64.self)
}
// Extract score (4 bytes, little-endian)
let score = outputBuffer[offset+8..<offset+12].withUnsafeBytes { ptr in
ptr.load(as: Float.self)
}
recommendations.append(Recommendation(contentId: id, score: score))
}
return recommendations
}
// MARK: - Learning
/// Record a user interaction for learning
/// - Parameter interaction: User interaction event
public func learn(interaction: UserInteraction) async throws {
updateLearningFunc?(
interaction.contentId,
interaction.interaction.rawValue,
interaction.timeSpent,
interaction.position
)
}
// MARK: - Similarity
/// Compute similarity between two content items
/// - Parameters:
/// - idA: First content ID
/// - idB: Second content ID
/// - Returns: Cosine similarity (-1.0 to 1.0)
public func similarity(between idA: UInt64, and idB: UInt64) -> Float {
return computeSimilarityFunc?(idA, idB) ?? 0.0
}
// MARK: - State Persistence
/// Save engine state for persistence
/// - Returns: Serialized state data
public func saveState() throws -> Data {
guard let saveFunc = saveStateFunc else {
throw WasmEngineError.functionNotFound("save_state")
}
let size = saveFunc()
guard size > 0 else {
throw WasmEngineError.saveFailed
}
// Read from WASM memory at the memory pool location
// In real implementation, get pointer from get_memory_ptr()
return Data() // Placeholder
}
/// Load engine state from persisted data
/// - Parameter data: Previously saved state data
public func loadState(_ data: Data) throws {
guard let loadFunc = loadStateFunc else {
throw WasmEngineError.functionNotFound("load_state")
}
let result = data.withUnsafeBytes { ptr in
loadFunc(ptr.baseAddress!.assumingMemoryBound(to: UInt8.self), UInt32(data.count))
}
guard result == 0 else {
throw WasmEngineError.loadFailed
}
}
// MARK: - Statistics
/// Get current exploration rate
public var explorationRate: Float {
return getExplorationRateFunc?() ?? 0.0
}
/// Get total learning updates
public var updateCount: UInt64 {
return getUpdateCountFunc?() ?? 0
}
/// Get embedding dimension
public var dimension: Int {
return Int(getEmbeddingDimFunc?() ?? 0)
}
}
// MARK: - State Manager
/// Actor for thread-safe state persistence
public actor WasmStateManager {
private let stateURL: URL
public init(stateURL: URL? = nil) {
self.stateURL = stateURL ?? FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("recommendation_state.bin")
}
/// Save state to disk
public func saveState(_ data: Data) async throws {
try data.write(to: stateURL, options: .atomic)
}
/// Load state from disk
public func loadState() async throws -> Data? {
guard FileManager.default.fileExists(atPath: stateURL.path) else {
return nil
}
return try Data(contentsOf: stateURL)
}
/// Delete saved state
public func clearState() async throws {
if FileManager.default.fileExists(atPath: stateURL.path) {
try FileManager.default.removeItem(at: stateURL)
}
}
}
// MARK: - Errors
public enum WasmEngineError: Error, LocalizedError {
case initializationFailed
case functionNotFound(String)
case embeddingFailed
case saveFailed
case loadFailed
case invalidInput(String)
public var errorDescription: String? {
switch self {
case .initializationFailed:
return "Failed to initialize WASM recommendation engine"
case .functionNotFound(let name):
return "WASM function not found: \(name)"
case .embeddingFailed:
return "Failed to generate content embedding"
case .saveFailed:
return "Failed to save engine state"
case .loadFailed:
return "Failed to load engine state"
case .invalidInput(let message):
return "Invalid input: \(message)"
}
}
}
// MARK: - Extensions
extension ContentMetadata {
/// Create from dictionary (useful for decoding from API)
public init?(from dict: [String: Any]) {
guard let id = dict["id"] as? UInt64,
let typeRaw = dict["type"] as? UInt8,
let type = ContentType(rawValue: typeRaw) else {
return nil
}
self.init(
id: id,
contentType: type,
durationSecs: dict["duration"] as? UInt32 ?? 0,
categoryFlags: dict["categories"] as? UInt32 ?? 0,
popularity: dict["popularity"] as? Float ?? 0.5,
recency: dict["recency"] as? Float ?? 0.5
)
}
}