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

@@ -0,0 +1,37 @@
import Foundation
import FoundationModels
/// The tool the on-device model calls to look things up in the user's library.
/// It is the bridge between `LanguageModelSession` (generation) and the
/// Spotlight index (retrieval) together they form on-device RAG over the
/// private bookmark corpus.
struct BookmarkSearchTool: Tool {
let name = "searchBookmarks"
let description = """
Search the user's saved bookmarks. Returns the most relevant bookmarks with \
their title, website, description, and URL. Call this for any question about \
what the user has saved, read, or wants to find again.
"""
@Generable
struct Arguments {
@Guide(description: "Keywords or a short phrase describing what to look for in the saved bookmarks")
var query: String
}
func call(arguments: Arguments) async throws -> String {
Log.tool.info("searchBookmarks invoked query=\(arguments.query, privacy: .public)")
let hits = await SpotlightBookmarkSearch.run(query: arguments.query, limit: 8)
Log.tool.info("searchBookmarks returned \(hits.count, privacy: .public) bookmarks")
guard !hits.isEmpty else {
return "No bookmarks matched \"\(arguments.query)\"."
}
return hits.enumerated().map { idx, b in
var line = "\(idx + 1). \(b.title)"
if !b.host.isEmpty { line += "\(b.host)" }
if !b.description.isEmpty { line += "\n \(b.description)" }
if !b.url.isEmpty { line += "\n \(b.url)" }
return line
}.joined(separator: "\n")
}
}