Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
347
vendor/ruvector/examples/wasm/ios/swift/HybridRecommendationService.swift
vendored
Normal file
347
vendor/ruvector/examples/wasm/ios/swift/HybridRecommendationService.swift
vendored
Normal 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
|
||||
}
|
||||
40
vendor/ruvector/examples/wasm/ios/swift/Package.swift
vendored
Normal file
40
vendor/ruvector/examples/wasm/ios/swift/Package.swift
vendored
Normal 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"
|
||||
),
|
||||
]
|
||||
)
|
||||
BIN
vendor/ruvector/examples/wasm/ios/swift/Resources/recommendation.wasm
vendored
Executable file
BIN
vendor/ruvector/examples/wasm/ios/swift/Resources/recommendation.wasm
vendored
Executable file
Binary file not shown.
637
vendor/ruvector/examples/wasm/ios/swift/RuvectorWasm.swift
vendored
Normal file
637
vendor/ruvector/examples/wasm/ios/swift/RuvectorWasm.swift
vendored
Normal 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
|
||||
180
vendor/ruvector/examples/wasm/ios/swift/Tests/RecommendationTests.swift
vendored
Normal file
180
vendor/ruvector/examples/wasm/ios/swift/Tests/RecommendationTests.swift
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
434
vendor/ruvector/examples/wasm/ios/swift/WasmRecommendationEngine.swift
vendored
Normal file
434
vendor/ruvector/examples/wasm/ios/swift/WasmRecommendationEngine.swift
vendored
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user