Small feature changes and bug fixes
This commit is contained in:
@@ -67,6 +67,49 @@ struct EmailLogRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
|
||||
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 {
|
||||
@@ -182,6 +225,73 @@ final class DatabaseService: Sendable {
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -574,4 +684,202 @@ final class DatabaseService: Sendable {
|
||||
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 = isoFormatter.string(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 = isoFormatter.string(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 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 = isoFormatter.string(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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user