import type { StorageAdapter } from '@mosaic/storage'; import type { MemoryAdapter, MemoryConfig, NewInsight, Insight, InsightSearchResult, } from '../types.js'; import type { EmbeddingProvider } from '../vector-store.js'; type KeywordConfig = Extract; const PREFERENCES = 'preferences'; const INSIGHTS = 'insights'; type PreferenceRecord = Record & { id: string; userId: string; key: string; value: unknown; category: string; }; type InsightRecord = Record & { id: string; userId: string; content: string; source: string; category: string; relevanceScore: number; metadata: Record; createdAt: string; updatedAt?: string; decayedAt?: string; }; export class KeywordAdapter implements MemoryAdapter { readonly name = 'keyword'; readonly embedder: EmbeddingProvider | null = null; private storage: StorageAdapter; constructor(config: KeywordConfig) { this.storage = config.storage; } /* ------------------------------------------------------------------ */ /* Preferences */ /* ------------------------------------------------------------------ */ async getPreference(userId: string, key: string): Promise { const row = await this.storage.findOne(PREFERENCES, { userId, key }); return row?.value ?? null; } async setPreference( userId: string, key: string, value: unknown, category?: string, ): Promise { const existing = await this.storage.findOne(PREFERENCES, { userId, key }); if (existing) { await this.storage.update(PREFERENCES, existing.id, { value, ...(category !== undefined ? { category } : {}), }); } else { await this.storage.create(PREFERENCES, { userId, key, value, category: category ?? 'general', }); } } async deletePreference(userId: string, key: string): Promise { const existing = await this.storage.findOne(PREFERENCES, { userId, key }); if (!existing) return false; return this.storage.delete(PREFERENCES, existing.id); } async listPreferences( userId: string, category?: string, ): Promise> { const filter: Record = { userId }; if (category !== undefined) filter.category = category; const rows = await this.storage.find(PREFERENCES, filter); return rows.map((r) => ({ key: r.key, value: r.value, category: r.category })); } /* ------------------------------------------------------------------ */ /* Insights */ /* ------------------------------------------------------------------ */ async storeInsight(insight: NewInsight): Promise { const now = new Date(); const row = await this.storage.create>(INSIGHTS, { userId: insight.userId, content: insight.content, source: insight.source, category: insight.category, relevanceScore: insight.relevanceScore, metadata: insight.metadata ?? {}, createdAt: now.toISOString(), }); return { id: row.id, userId: insight.userId, content: insight.content, source: insight.source, category: insight.category, relevanceScore: insight.relevanceScore, metadata: insight.metadata, createdAt: now, }; } async getInsight(id: string): Promise { const row = await this.storage.read(INSIGHTS, id); if (!row) return null; return toInsight(row); } async searchInsights( userId: string, query: string, opts?: { limit?: number; embedding?: number[] }, ): Promise { const limit = opts?.limit ?? 10; const words = query .toLowerCase() .split(/\s+/) .filter((w) => w.length > 0); if (words.length === 0) return []; const rows = await this.storage.find(INSIGHTS, { userId }); const scored: InsightSearchResult[] = []; for (const row of rows) { const content = row.content.toLowerCase(); let score = 0; for (const word of words) { if (content.includes(word)) score++; } if (score > 0) { scored.push({ id: row.id, content: row.content, score, metadata: row.metadata ?? undefined, }); } } scored.sort((a, b) => b.score - a.score); return scored.slice(0, limit); } async deleteInsight(id: string): Promise { return this.storage.delete(INSIGHTS, id); } /* ------------------------------------------------------------------ */ /* Lifecycle */ /* ------------------------------------------------------------------ */ async close(): Promise { // no-op — storage adapter manages its own lifecycle } } /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ function toInsight(row: InsightRecord): Insight { return { id: row.id, userId: row.userId, content: row.content, source: row.source, category: row.category, relevanceScore: row.relevanceScore, metadata: row.metadata ?? undefined, createdAt: new Date(row.createdAt), updatedAt: row.updatedAt ? new Date(row.updatedAt) : undefined, decayedAt: row.decayedAt ? new Date(row.decayedAt) : undefined, }; }