Files
oai-swift/oAI/Services/DatabaseService.swift
T
runeandClaude Sonnet 5 bd686873c4 Update commercial licensing contact URL to oai.pm
Consolidates the mac.oai.pm subdomain references (introduced in the
PolyForm Noncommercial relicense) to the root oai.pm domain, across
the LICENSE file, README, and all Swift source file headers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:33:53 +02:00

956 lines
33 KiB
Swift

//
// DatabaseService.swift
// oAI
//
// SQLite persistence layer for conversations using GRDB
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of oAI.
//
// oAI is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling oAI or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://oai.pm>.
import Foundation
import GRDB
import os
// MARK: - Database Record Types
struct ConversationRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
static let databaseTableName = "conversations"
var id: String
var name: String
var createdAt: String
var updatedAt: String
var primaryModel: String?
}
struct MessageRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
static let databaseTableName = "messages"
var id: String
var conversationId: String
var role: String
var content: String
var tokens: Int?
var cost: Double?
var timestamp: String
var sortOrder: Int
var modelId: String?
}
struct SettingRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
static let databaseTableName = "settings"
var key: String
var value: String
}
struct HistoryRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
static let databaseTableName = "command_history"
var id: String
var input: String
var timestamp: String
}
struct EmailLogRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
static let databaseTableName = "email_logs"
var id: String
var timestamp: String
var sender: String
var subject: String
var emailContent: String
var aiResponse: String?
var status: String // "success" or "error"
var errorMessage: String?
var tokens: Int?
var cost: Double?
var responseTime: Double?
var modelId: String?
}
struct MessageMetadataRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
static let databaseTableName = "message_metadata"
var message_id: String
var importance_score: Double
var user_starred: Int
var summary: String?
var chunk_index: Int
}
struct MessageEmbeddingRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
static let databaseTableName = "message_embeddings"
var message_id: String
var embedding: Data
var embedding_model: String
var embedding_dimension: Int
var created_at: String
}
struct ConversationEmbeddingRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
static let databaseTableName = "conversation_embeddings"
var conversation_id: String
var embedding: Data
var embedding_model: String
var embedding_dimension: Int
var created_at: String
}
struct ConversationSummaryRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
static let databaseTableName = "conversation_summaries"
var id: String
var conversation_id: String
var start_message_index: Int
var end_message_index: Int
var summary: String
var token_count: Int?
var created_at: String
var summary_model: String?
}
// MARK: - DatabaseService
final class DatabaseService: Sendable {
nonisolated static let shared = DatabaseService()
private let dbQueue: DatabaseQueue
// Command history limit - keep most recent 5000 entries
private nonisolated static let maxHistoryEntries = 5000
// ISO8601DateFormatter is @MainActor in macOS 27. Use Date.ISO8601FormatStyle (value type, Sendable).
private nonisolated static let isoStyle = Date.ISO8601FormatStyle(
dateSeparator: .dash,
dateTimeSeparator: .standard,
timeSeparator: .colon,
timeZoneSeparator: .colon,
includingFractionalSeconds: true,
timeZone: .gmt
)
private nonisolated static func isoString(from date: Date) -> String {
isoStyle.format(date)
}
private nonisolated static func isoDate(from string: String) -> Date? {
(try? isoStyle.parse(string)) ?? (try? Date(string, strategy: .iso8601))
}
nonisolated private init() {
let fileManager = FileManager.default
let appSupport = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let dbDirectory = appSupport.appendingPathComponent("oAI", isDirectory: true)
try! fileManager.createDirectory(at: dbDirectory, withIntermediateDirectories: true)
let dbPath = dbDirectory.appendingPathComponent("oai_conversations.db").path
Log.db.info("Opening database at \(dbPath)")
dbQueue = try! DatabaseQueue(path: dbPath)
try! migrator.migrate(dbQueue)
}
private nonisolated var migrator: DatabaseMigrator {
var migrator = DatabaseMigrator()
migrator.registerMigration("v1") { db in
try db.create(table: "conversations") { t in
t.primaryKey("id", .text)
t.column("name", .text).notNull()
t.column("createdAt", .text).notNull()
t.column("updatedAt", .text).notNull()
}
try db.create(table: "messages") { t in
t.primaryKey("id", .text)
t.column("conversationId", .text).notNull()
.references("conversations", onDelete: .cascade)
t.column("role", .text).notNull()
t.column("content", .text).notNull()
t.column("tokens", .integer)
t.column("cost", .double)
t.column("timestamp", .text).notNull()
t.column("sortOrder", .integer).notNull()
}
try db.create(
index: "messages_on_conversationId",
on: "messages",
columns: ["conversationId"]
)
}
migrator.registerMigration("v2") { db in
try db.create(table: "settings") { t in
t.primaryKey("key", .text)
t.column("value", .text).notNull()
}
}
migrator.registerMigration("v3") { db in
try db.create(table: "command_history") { t in
t.primaryKey("id", .text)
t.column("input", .text).notNull()
t.column("timestamp", .text).notNull()
}
try db.create(
index: "command_history_on_timestamp",
on: "command_history",
columns: ["timestamp"]
)
}
migrator.registerMigration("v4") { db in
// Add modelId to messages table (nullable for existing messages)
try db.alter(table: "messages") { t in
t.add(column: "modelId", .text)
}
// Add primaryModel to conversations table (nullable)
try db.alter(table: "conversations") { t in
t.add(column: "primaryModel", .text)
}
}
migrator.registerMigration("v5") { db in
// Email handler logs
try db.create(table: "email_logs") { t in
t.primaryKey("id", .text)
t.column("timestamp", .text).notNull()
t.column("sender", .text).notNull()
t.column("subject", .text).notNull()
t.column("emailContent", .text).notNull()
t.column("aiResponse", .text)
t.column("status", .text).notNull() // "success" or "error"
t.column("errorMessage", .text)
t.column("tokens", .integer)
t.column("cost", .double)
t.column("responseTime", .double)
t.column("modelId", .text)
}
try db.create(
index: "email_logs_on_timestamp",
on: "email_logs",
columns: ["timestamp"]
)
}
migrator.registerMigration("v6") { db in
// Message metadata for smart context selection
try db.create(table: "message_metadata") { t in
t.primaryKey("message_id", .text)
.references("messages", onDelete: .cascade)
t.column("importance_score", .double).notNull().defaults(to: 0.0)
t.column("user_starred", .integer).notNull().defaults(to: 0)
t.column("summary", .text)
t.column("chunk_index", .integer).notNull().defaults(to: 0)
}
try db.create(
index: "idx_message_metadata_importance",
on: "message_metadata",
columns: ["importance_score"]
)
try db.create(
index: "idx_message_metadata_starred",
on: "message_metadata",
columns: ["user_starred"]
)
}
migrator.registerMigration("v7") { db in
// Message embeddings for semantic search
try db.create(table: "message_embeddings") { t in
t.primaryKey("message_id", .text)
.references("messages", onDelete: .cascade)
t.column("embedding", .blob).notNull()
t.column("embedding_model", .text).notNull()
t.column("embedding_dimension", .integer).notNull()
t.column("created_at", .text).notNull()
}
// Conversation embeddings (aggregate of all messages)
try db.create(table: "conversation_embeddings") { t in
t.primaryKey("conversation_id", .text)
.references("conversations", onDelete: .cascade)
t.column("embedding", .blob).notNull()
t.column("embedding_model", .text).notNull()
t.column("embedding_dimension", .integer).notNull()
t.column("created_at", .text).notNull()
}
}
migrator.registerMigration("v8") { db in
// Conversation summaries for progressive summarization
try db.create(table: "conversation_summaries") { t in
t.primaryKey("id", .text)
t.column("conversation_id", .text).notNull()
.references("conversations", onDelete: .cascade)
t.column("start_message_index", .integer).notNull()
t.column("end_message_index", .integer).notNull()
t.column("summary", .text).notNull()
t.column("token_count", .integer)
t.column("created_at", .text).notNull()
t.column("summary_model", .text)
}
try db.create(
index: "idx_conversation_summaries_conv",
on: "conversation_summaries",
columns: ["conversation_id"]
)
}
return migrator
}
// MARK: - Settings Operations
nonisolated func loadAllSettings() throws -> [String: String] {
try dbQueue.read { db in
let records = try SettingRecord.fetchAll(db)
return Dictionary(uniqueKeysWithValues: records.map { ($0.key, $0.value) })
}
}
nonisolated func setSetting(key: String, value: String) {
try? dbQueue.write { db in
let record = SettingRecord(key: key, value: value)
try record.save(db)
}
}
nonisolated func deleteSetting(key: String) {
try? dbQueue.write { db in
_ = try SettingRecord.deleteOne(db, key: key)
}
}
// MARK: - Encrypted Settings Operations
/// Store an encrypted setting (for sensitive data like API keys)
nonisolated func setEncryptedSetting(key: String, value: String) throws {
let encryptedValue = try EncryptionService.shared.encrypt(value)
try dbQueue.write { db in
let record = SettingRecord(key: "encrypted_\(key)", value: encryptedValue)
try record.save(db)
}
}
/// Retrieve and decrypt an encrypted setting
nonisolated func getEncryptedSetting(key: String) throws -> String? {
let encryptedValue = try dbQueue.read { db in
try SettingRecord.fetchOne(db, key: "encrypted_\(key)")?.value
}
guard let encryptedValue = encryptedValue else {
return nil
}
return try EncryptionService.shared.decrypt(encryptedValue)
}
/// Delete an encrypted setting
nonisolated func deleteEncryptedSetting(key: String) {
try? dbQueue.write { db in
_ = try SettingRecord.deleteOne(db, key: "encrypted_\(key)")
}
}
// MARK: - Conversation Operations
nonisolated func saveConversation(name: String, messages: [Message]) throws -> Conversation {
return try saveConversation(id: UUID(), name: name, messages: messages, primaryModel: nil)
}
nonisolated func saveConversation(id: UUID, name: String, messages: [Message], primaryModel: String?) throws -> Conversation {
Log.db.info("Saving conversation '\(name)' with \(messages.count) messages (primaryModel: \(primaryModel ?? "none"))")
let now = Date()
let nowString = Self.isoString(from: now)
let convRecord = ConversationRecord(
id: id.uuidString,
name: name,
createdAt: nowString,
updatedAt: nowString,
primaryModel: primaryModel
)
let messageRecords = messages.enumerated().compactMap { index, msg -> MessageRecord? in
guard msg.role != .system else { return nil }
return MessageRecord(
id: UUID().uuidString,
conversationId: id.uuidString,
role: msg.role.rawValue,
content: msg.content,
tokens: msg.tokens,
cost: msg.cost,
timestamp: Self.isoString(from: msg.timestamp),
sortOrder: index,
modelId: msg.modelId
)
}
try dbQueue.write { db in
try convRecord.insert(db)
for record in messageRecords {
try record.insert(db)
}
}
let savedMessages = messages.filter { $0.role != .system }
return Conversation(
id: id,
name: name,
messages: savedMessages,
createdAt: now,
updatedAt: now,
primaryModel: primaryModel
)
}
/// Update an existing conversation in-place: rename it, replace all its messages.
nonisolated func updateConversation(id: UUID, name: String, messages: [Message], primaryModel: String?) throws {
let nowString = Self.isoString(from: Date())
let messageRecords = messages.enumerated().compactMap { index, msg -> MessageRecord? in
guard msg.role != .system else { return nil }
return MessageRecord(
id: UUID().uuidString,
conversationId: id.uuidString,
role: msg.role.rawValue,
content: msg.content,
tokens: msg.tokens,
cost: msg.cost,
timestamp: Self.isoString(from: msg.timestamp),
sortOrder: index,
modelId: msg.modelId
)
}
try dbQueue.write { db in
try db.execute(
sql: "UPDATE conversations SET name = ?, updatedAt = ?, primaryModel = ? WHERE id = ?",
arguments: [name, nowString, primaryModel, id.uuidString]
)
try db.execute(
sql: "DELETE FROM messages WHERE conversationId = ?",
arguments: [id.uuidString]
)
for record in messageRecords {
try record.insert(db)
}
}
}
nonisolated func loadConversation(id: UUID) throws -> (Conversation, [Message])? {
try dbQueue.read { db in
guard let convRecord = try ConversationRecord.fetchOne(db, key: id.uuidString) else {
return nil
}
let messageRecords = try MessageRecord
.filter(Column("conversationId") == id.uuidString)
.order(Column("sortOrder"))
.fetchAll(db)
let messages = messageRecords.compactMap { record -> Message? in
guard let msgId = UUID(uuidString: record.id),
let role = MessageRole(rawValue: record.role),
let timestamp = Self.isoDate(from: record.timestamp)
else { return nil }
let starred = (try? MessageMetadataRecord.fetchOne(db, key: record.id))?.user_starred == 1
var message = Message(
id: msgId,
role: role,
content: record.content,
tokens: record.tokens,
cost: record.cost,
timestamp: timestamp,
modelId: record.modelId
)
message.isStarred = starred
return message
}
guard let convId = UUID(uuidString: convRecord.id),
let createdAt = Self.isoDate(from: convRecord.createdAt),
let updatedAt = Self.isoDate(from: convRecord.updatedAt)
else { return nil }
let conversation = Conversation(
id: convId,
name: convRecord.name,
messages: messages,
createdAt: createdAt,
updatedAt: updatedAt,
primaryModel: convRecord.primaryModel
)
return (conversation, messages)
}
}
nonisolated func listConversations() throws -> [Conversation] {
try dbQueue.read { db in
let records = try ConversationRecord
.order(Column("updatedAt").desc)
.fetchAll(db)
return records.compactMap { record -> Conversation? in
guard let id = UUID(uuidString: record.id),
let createdAt = Self.isoDate(from: record.createdAt),
let updatedAt = Self.isoDate(from: record.updatedAt)
else { return nil }
// Fetch message count without loading all messages
let messageCount = (try? MessageRecord
.filter(Column("conversationId") == record.id)
.fetchCount(db)) ?? 0
// Get last message (for date + model fallback)
let lastMsg = try? MessageRecord
.filter(Column("conversationId") == record.id)
.order(Column("sortOrder").desc)
.fetchOne(db)
let lastDate = lastMsg.flatMap { Self.isoDate(from: $0.timestamp) } ?? updatedAt
// Derive primary model: prefer the stored field, fall back to last message's modelId
let primaryModel = record.primaryModel ?? lastMsg?.modelId
// Create conversation with empty messages array but correct metadata
var conv = Conversation(
id: id,
name: record.name,
messages: Array(repeating: Message(role: .user, content: ""), count: messageCount),
createdAt: createdAt,
updatedAt: lastDate,
primaryModel: primaryModel
)
conv.updatedAt = lastDate
return conv
}
}
}
nonisolated func deleteConversation(id: UUID) throws -> Bool {
Log.db.info("Deleting conversation \(id.uuidString)")
return try dbQueue.write { db in
try MessageRecord.filter(Column("conversationId") == id.uuidString).deleteAll(db)
return try ConversationRecord.deleteOne(db, key: id.uuidString)
}
}
nonisolated func deleteConversation(name: String) throws -> Bool {
try dbQueue.write { db in
guard let record = try ConversationRecord
.filter(Column("name") == name)
.fetchOne(db)
else { return false }
try MessageRecord.filter(Column("conversationId") == record.id).deleteAll(db)
return try ConversationRecord.deleteOne(db, key: record.id)
}
}
nonisolated func updateConversation(id: UUID, name: String?, messages: [Message]?) throws -> Bool {
try dbQueue.write { db in
guard var convRecord = try ConversationRecord.fetchOne(db, key: id.uuidString) else {
return false
}
if let name = name {
convRecord.name = name
}
convRecord.updatedAt = Self.isoString(from: Date())
try convRecord.update(db)
if let messages = messages {
try MessageRecord.filter(Column("conversationId") == id.uuidString).deleteAll(db)
let messageRecords = messages.enumerated().compactMap { index, msg -> MessageRecord? in
guard msg.role != .system else { return nil }
return MessageRecord(
id: UUID().uuidString,
conversationId: id.uuidString,
role: msg.role.rawValue,
content: msg.content,
tokens: msg.tokens,
cost: msg.cost,
timestamp: Self.isoString(from: msg.timestamp),
sortOrder: index
)
}
for record in messageRecords {
try record.insert(db)
}
}
return true
}
}
// MARK: - Command History Operations
nonisolated func saveCommandHistory(input: String) {
let now = Date()
let record = HistoryRecord(
id: UUID().uuidString,
input: input,
timestamp: Self.isoString(from: now)
)
try? dbQueue.write { db in
try record.insert(db)
// Clean up old entries if we exceed the limit
let count = try HistoryRecord.fetchCount(db)
if count > Self.maxHistoryEntries {
// Delete oldest entries to get back to limit
let excess = count - Self.maxHistoryEntries
try db.execute(
sql: """
DELETE FROM command_history
WHERE id IN (
SELECT id FROM command_history
ORDER BY timestamp ASC
LIMIT ?
)
""",
arguments: [excess]
)
}
}
}
nonisolated func loadCommandHistory() throws -> [(input: String, timestamp: Date)] {
try dbQueue.read { db in
let records = try HistoryRecord
.order(Column("timestamp").desc)
.fetchAll(db)
return records.compactMap { record in
guard let date = Self.isoDate(from: record.timestamp) else {
return nil
}
return (input: record.input, timestamp: date)
}
}
}
nonisolated func searchCommandHistory(query: String) throws -> [(input: String, timestamp: Date)] {
try dbQueue.read { db in
let records = try HistoryRecord
.filter(Column("input").like("%\(query)%"))
.order(Column("timestamp").desc)
.fetchAll(db)
return records.compactMap { record in
guard let date = Self.isoDate(from: record.timestamp) else {
return nil
}
return (input: record.input, timestamp: date)
}
}
}
// MARK: - Email Log Operations
nonisolated func saveEmailLog(_ log: EmailLog) {
let record = EmailLogRecord(
id: log.id.uuidString,
timestamp: Self.isoString(from: log.timestamp),
sender: log.sender,
subject: log.subject,
emailContent: log.emailContent,
aiResponse: log.aiResponse,
status: log.status.rawValue,
errorMessage: log.errorMessage,
tokens: log.tokens,
cost: log.cost,
responseTime: log.responseTime,
modelId: log.modelId
)
try? dbQueue.write { db in
try record.insert(db)
}
}
nonisolated func loadEmailLogs(limit: Int = 100) throws -> [EmailLog] {
try dbQueue.read { db in
let records = try EmailLogRecord
.order(Column("timestamp").desc)
.limit(limit)
.fetchAll(db)
return records.compactMap { record in
guard let timestamp = Self.isoDate(from: record.timestamp),
let status = EmailLogStatus(rawValue: record.status),
let id = UUID(uuidString: record.id) else {
return nil
}
return EmailLog(
id: id,
timestamp: timestamp,
sender: record.sender,
subject: record.subject,
emailContent: record.emailContent,
aiResponse: record.aiResponse,
status: status,
errorMessage: record.errorMessage,
tokens: record.tokens,
cost: record.cost,
responseTime: record.responseTime,
modelId: record.modelId
)
}
}
}
nonisolated func deleteEmailLog(id: UUID) {
try? dbQueue.write { db in
try db.execute(
sql: "DELETE FROM email_logs WHERE id = ?",
arguments: [id.uuidString]
)
}
}
nonisolated func clearEmailLogs() {
try? dbQueue.write { db in
try db.execute(sql: "DELETE FROM email_logs")
}
}
nonisolated func getEmailLogCount() throws -> Int {
try dbQueue.read { db in
try EmailLogRecord.fetchCount(db)
}
}
// MARK: - Message Metadata Operations
nonisolated func getMessageMetadata(messageId: UUID) throws -> MessageMetadataRecord? {
try dbQueue.read { db in
try MessageMetadataRecord.fetchOne(db, key: messageId.uuidString)
}
}
nonisolated func setMessageStarred(messageId: UUID, starred: Bool) throws {
try dbQueue.write { db in
// Check if metadata exists
if var existing = try MessageMetadataRecord.fetchOne(db, key: messageId.uuidString) {
existing.user_starred = starred ? 1 : 0
try existing.update(db)
} else {
// Create new metadata record
let record = MessageMetadataRecord(
message_id: messageId.uuidString,
importance_score: 0.0,
user_starred: starred ? 1 : 0,
summary: nil,
chunk_index: 0
)
try record.insert(db)
}
}
}
nonisolated func setMessageImportance(messageId: UUID, score: Double) throws {
try dbQueue.write { db in
if var existing = try MessageMetadataRecord.fetchOne(db, key: messageId.uuidString) {
existing.importance_score = score
try existing.update(db)
} else {
let record = MessageMetadataRecord(
message_id: messageId.uuidString,
importance_score: score,
user_starred: 0,
summary: nil,
chunk_index: 0
)
try record.insert(db)
}
}
}
nonisolated func getStarredMessages(conversationId: UUID) throws -> [String] {
try dbQueue.read { db in
let sql = """
SELECT mm.message_id
FROM message_metadata mm
JOIN messages m ON m.id = mm.message_id
WHERE m.conversationId = ? AND mm.user_starred = 1
ORDER BY m.sortOrder
"""
return try String.fetchAll(db, sql: sql, arguments: [conversationId.uuidString])
}
}
// MARK: - Embedding Operations
nonisolated func saveMessageEmbedding(messageId: UUID, embedding: Data, model: String, dimension: Int) throws {
let now = Self.isoString(from: Date())
let record = MessageEmbeddingRecord(
message_id: messageId.uuidString,
embedding: embedding,
embedding_model: model,
embedding_dimension: dimension,
created_at: now
)
try dbQueue.write { db in
try record.save(db)
}
}
nonisolated func getMessageEmbedding(messageId: UUID) throws -> Data? {
try dbQueue.read { db in
try MessageEmbeddingRecord.fetchOne(db, key: messageId.uuidString)?.embedding
}
}
nonisolated func saveConversationEmbedding(conversationId: UUID, embedding: Data, model: String, dimension: Int) throws {
let now = Self.isoString(from: Date())
let record = ConversationEmbeddingRecord(
conversation_id: conversationId.uuidString,
embedding: embedding,
embedding_model: model,
embedding_dimension: dimension,
created_at: now
)
try dbQueue.write { db in
try record.save(db)
}
}
nonisolated func getConversationEmbedding(conversationId: UUID) throws -> Data? {
try dbQueue.read { db in
try ConversationEmbeddingRecord.fetchOne(db, key: conversationId.uuidString)?.embedding
}
}
nonisolated func getAllConversationEmbeddings() throws -> [(UUID, Data)] {
try dbQueue.read { db in
let records = try ConversationEmbeddingRecord.fetchAll(db)
return records.compactMap { record in
guard let id = UUID(uuidString: record.conversation_id) else { return nil }
return (id, record.embedding)
}
}
}
/// Search conversations by semantic similarity
nonisolated func searchConversationsBySemantic(queryEmbedding: [Float], limit: Int = 10) throws -> [(Conversation, Float)] {
// Get all conversations
let allConversations = try listConversations()
// Get all conversation embeddings
let embeddingData = try getAllConversationEmbeddings()
let embeddingMap = Dictionary(uniqueKeysWithValues: embeddingData)
// Calculate similarity scores
var results: [(Conversation, Float)] = []
for conv in allConversations {
guard let embeddingData = embeddingMap[conv.id] else { continue }
// Deserialize embedding
let embedding = deserializeEmbedding(embeddingData)
// Calculate cosine similarity
let similarity = EmbeddingService.shared.cosineSimilarity(queryEmbedding, embedding)
results.append((conv, similarity))
}
// Sort by similarity (highest first) and take top N
results.sort { $0.1 > $1.1 }
return Array(results.prefix(limit))
}
private nonisolated func deserializeEmbedding(_ data: Data) -> [Float] {
var embedding: [Float] = []
embedding.reserveCapacity(data.count / 4)
for offset in stride(from: 0, to: data.count, by: 4) {
let bytes = data.subdata(in: offset..<(offset + 4))
let bitPattern = bytes.withUnsafeBytes { $0.load(as: UInt32.self) }
let value = Float(bitPattern: UInt32(littleEndian: bitPattern))
embedding.append(value)
}
return embedding
}
// MARK: - Conversation Summary Operations
nonisolated func saveConversationSummary(
conversationId: UUID,
startIndex: Int,
endIndex: Int,
summary: String,
model: String?,
tokenCount: Int?
) throws {
let now = Self.isoString(from: Date())
let record = ConversationSummaryRecord(
id: UUID().uuidString,
conversation_id: conversationId.uuidString,
start_message_index: startIndex,
end_message_index: endIndex,
summary: summary,
token_count: tokenCount,
created_at: now,
summary_model: model
)
try dbQueue.write { db in
try record.insert(db)
}
}
nonisolated func getConversationSummaries(conversationId: UUID) throws -> [ConversationSummaryRecord] {
try dbQueue.read { db in
try ConversationSummaryRecord
.filter(Column("conversation_id") == conversationId.uuidString)
.order(Column("start_message_index"))
.fetchAll(db)
}
}
nonisolated func hasSummaryForRange(conversationId: UUID, startIndex: Int, endIndex: Int) throws -> Bool {
try dbQueue.read { db in
let count = try ConversationSummaryRecord
.filter(Column("conversation_id") == conversationId.uuidString)
.filter(Column("start_message_index") == startIndex)
.filter(Column("end_message_index") == endIndex)
.fetchCount(db)
return count > 0
}
}
}