import Foundation import Observation import FoundationModels /// Generation half of on-device RAG. Wraps a `LanguageModelSession` configured /// with `BookmarkSearchTool`, so every question is answered by Apple's /// on-device model grounded in the user's own bookmarks — no network, no /// third-party LLM. Mirrors the `ClaudeService` role, but fully on-device. @MainActor @Observable final class BookmarkAssistant { enum Status: Equatable { case checking case ready case unavailable(String) } private(set) var status: Status = .checking private var session: LanguageModelSession? init() { configure() } private func configure() { switch SystemLanguageModel.default.availability { case .available: session = LanguageModelSession( tools: [BookmarkSearchTool()], instructions: """ You are an assistant inside a bookmarks app. The user's saved bookmarks \ are your only source of truth. To answer any question, call the \ searchBookmarks tool to look things up — never rely on outside knowledge \ or invent links. Ground every answer in the returned bookmarks, refer to \ them by title, and include their URLs when relevant. If nothing relevant \ is found, say so plainly. Keep answers concise. """ ) status = .ready Log.assistant.info("On-device model available; session ready") case .unavailable(let reason): status = .unavailable(Self.message(for: reason)) Log.assistant.notice("On-device model unavailable: \(String(describing: reason), privacy: .public)") } } /// Ask a question; the model retrieves from Spotlight via the tool as needed. func ask(_ question: String) async throws -> String { guard let session else { Log.assistant.error("ask() with no session (model unavailable)") throw AssistantError.unavailable } Log.assistant.info("Ask: \(question, privacy: .public)") let start = Date() do { let answer = try await session.respond(to: question).content let ms = Int(Date().timeIntervalSince(start) * 1000) Log.assistant.info("Answered in \(ms, privacy: .public)ms, \(answer.count, privacy: .public) chars") return answer } catch { Log.assistant.error("Ask failed: \(error.localizedDescription, privacy: .public)") throw error } } private static func message(for reason: SystemLanguageModel.Availability.UnavailableReason) -> String { switch reason { case .deviceNotEligible: return "This device doesn't support Apple Intelligence." case .appleIntelligenceNotEnabled: return "Turn on Apple Intelligence in Settings to ask your bookmarks." case .modelNotReady: return "The on-device model is still downloading. Try again shortly." @unknown default: return "On-device intelligence isn't available right now." } } enum AssistantError: LocalizedError { case unavailable var errorDescription: String? { "On-device intelligence is unavailable." } } }