feat(ai): on-device RAG over bookmarks + Spotlight indexing + unified logging
All checks were successful
CI / build-and-deploy (push) Successful in 22s

- SpotlightIndexer: actually push BookmarkEntity into the Spotlight index via
  indexAppEntities (IndexedEntity conformance alone indexes nothing). Wired into
  every sync/mutation point. This is what lets Apple Intelligence answer
  free-form Siri questions grounded in the user's bookmarks.
- On-device RAG: BookmarkAssistant (LanguageModelSession) + BookmarkSearchTool
  + SpotlightBookmarkSearch (CSSearchQuery retrieval), surfaced via AskView with
  lightweight Markdown rendering of answers.
- Log: os.Logger facility (com.magicive.marks) replacing ad-hoc print(); shared
  with ShareExtension. ClaudeService now logs server error bodies on non-2xx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krishna Kumar
2026-06-24 12:55:32 -05:00
parent e82e69c5f3
commit 96ea5fe6f6
14 changed files with 499 additions and 18 deletions

View File

@@ -42,6 +42,7 @@ final class BookmarksViewModel {
WidgetDataStore.saveBookmarks(bookmarks.prefix(20).map {
WidgetBookmark(url: $0.url, title: $0.displayTitle, domain: $0.domain, faviconUrl: $0.faviconUrl)
})
Task { await SpotlightIndexer.index(bookmarks) }
} catch {
self.error = error.localizedDescription
}
@@ -57,6 +58,8 @@ final class BookmarksViewModel {
bookmarks.append(contentsOf: response.results)
nextPageUrl = response.next
restoreAIData()
let added = response.results
Task { await SpotlightIndexer.index(added) }
} catch {
self.error = error.localizedDescription
}
@@ -102,21 +105,24 @@ final class BookmarksViewModel {
let create = BookmarkCreate(url: url, title: title, tagNames: tags, isArchived: false, unread: false, shared: false)
let bookmark = try await api.createBookmark(create)
bookmarks.insert(bookmark, at: 0)
print("[AI] addBookmark: saved id=\(bookmark.id)")
Task { await SpotlightIndexer.index([bookmark]) }
Log.ai.info("addBookmark saved id=\(bookmark.id, privacy: .public)")
Task {
print("[AI] addBookmark: starting enrich for id=\(bookmark.id) url=\(bookmark.url)")
Log.ai.info("addBookmark enrich start id=\(bookmark.id, privacy: .public)")
do {
let (summary, aiTags) = try await claude.enrich(bookmark: bookmark)
print("[AI] addBookmark: enrich success summary=\(summary.prefix(60)) tags=\(aiTags)")
Log.ai.info("addBookmark enrich ok id=\(bookmark.id, privacy: .public) tags=\(aiTags.count, privacy: .public)")
guard let i = bookmarks.firstIndex(where: { $0.id == bookmark.id }) else {
print("[AI] addBookmark: bookmark id=\(bookmark.id) not found in list after enrich")
Log.ai.notice("addBookmark id=\(bookmark.id, privacy: .public) gone before enrich applied")
return
}
bookmarks[i].aiSummary = summary
bookmarks[i].aiTags = aiTags
saveAIData(for: bookmarks[i])
let enriched = bookmarks[i]
Task { await SpotlightIndexer.index([enriched]) }
} catch {
print("[AI] addBookmark: enrich FAILED \(error)")
Log.ai.error("addBookmark enrich failed: \(error.localizedDescription, privacy: .public)")
self.error = "AI enrichment failed: \(error.localizedDescription)"
}
}
@@ -126,6 +132,7 @@ final class BookmarksViewModel {
do {
try await api.deleteBookmark(id: bookmark.id)
bookmarks.removeAll { $0.id == bookmark.id }
Task { await SpotlightIndexer.remove(ids: [bookmark.id]) }
} catch {
self.error = error.localizedDescription
}
@@ -135,6 +142,7 @@ final class BookmarksViewModel {
do {
try await api.archiveBookmark(id: bookmark.id)
bookmarks.removeAll { $0.id == bookmark.id }
Task { await SpotlightIndexer.remove(ids: [bookmark.id]) }
} catch {
self.error = error.localizedDescription
}
@@ -180,6 +188,8 @@ final class BookmarksViewModel {
bookmarks[i].aiSummary = summary
bookmarks[i].aiTags = tags
saveAIData(for: bookmarks[i])
let enriched = bookmarks[i]
Task { await SpotlightIndexer.index([enriched]) }
enrichmentProgress = Double(done + 1) / Double(toEnrich.count)
} catch {
break
@@ -196,23 +206,25 @@ final class BookmarksViewModel {
enrichTask = Task { [weak self] in
guard let self else { return }
let indices = await MainActor.run { bookmarks.indices.filter { bookmarks[$0].aiSummary == nil }.prefix(5) }
print("[AI] startEnrichment: \(indices.count) bookmarks to enrich")
Log.ai.info("startEnrichment: \(indices.count, privacy: .public) to enrich")
for i in indices {
guard !Task.isCancelled else { return }
let claude = await MainActor.run(body: { self.claude })
do {
let bm = await MainActor.run { bookmarks[i] }
print("[AI] startEnrichment: enriching id=\(bm.id) \(bm.url)")
Log.ai.debug("startEnrichment enriching id=\(bm.id, privacy: .public)")
let (summary, tags) = try await claude.enrich(bookmark: bm)
print("[AI] startEnrichment: done id=\(bm.id) summary=\(summary.prefix(60))")
Log.ai.debug("startEnrichment done id=\(bm.id, privacy: .public)")
await MainActor.run {
guard i < bookmarks.count else { return }
bookmarks[i].aiSummary = summary
bookmarks[i].aiTags = tags
saveAIData(for: bookmarks[i])
let enriched = bookmarks[i]
Task { await SpotlightIndexer.index([enriched]) }
}
} catch {
print("[AI] startEnrichment: enrich FAILED id=\(i) \(error)")
Log.ai.error("startEnrichment failed idx=\(i, privacy: .public): \(error.localizedDescription, privacy: .public)")
break
}
}
@@ -239,7 +251,7 @@ final class BookmarksViewModel {
let data = try Self.cacheEncoder.encode(list)
try data.write(to: cacheFileUrl, options: .atomic)
} catch {
print("[Cache] saveToCache failed: \(error)")
Log.sync.error("saveToCache failed: \(error.localizedDescription, privacy: .public)")
}
}