196 lines
5.7 KiB
TypeScript
196 lines
5.7 KiB
TypeScript
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<MemoryConfig, { type: 'keyword' }>;
|
|
|
|
const PREFERENCES = 'preferences';
|
|
const INSIGHTS = 'insights';
|
|
|
|
type PreferenceRecord = Record<string, unknown> & {
|
|
id: string;
|
|
userId: string;
|
|
key: string;
|
|
value: unknown;
|
|
category: string;
|
|
};
|
|
|
|
type InsightRecord = Record<string, unknown> & {
|
|
id: string;
|
|
userId: string;
|
|
content: string;
|
|
source: string;
|
|
category: string;
|
|
relevanceScore: number;
|
|
metadata: Record<string, unknown>;
|
|
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<unknown | null> {
|
|
const row = await this.storage.findOne<PreferenceRecord>(PREFERENCES, { userId, key });
|
|
return row?.value ?? null;
|
|
}
|
|
|
|
async setPreference(
|
|
userId: string,
|
|
key: string,
|
|
value: unknown,
|
|
category?: string,
|
|
): Promise<void> {
|
|
const existing = await this.storage.findOne<PreferenceRecord>(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<boolean> {
|
|
const existing = await this.storage.findOne<PreferenceRecord>(PREFERENCES, { userId, key });
|
|
if (!existing) return false;
|
|
return this.storage.delete(PREFERENCES, existing.id);
|
|
}
|
|
|
|
async listPreferences(
|
|
userId: string,
|
|
category?: string,
|
|
): Promise<Array<{ key: string; value: unknown; category: string }>> {
|
|
const filter: Record<string, unknown> = { userId };
|
|
if (category !== undefined) filter.category = category;
|
|
|
|
const rows = await this.storage.find<PreferenceRecord>(PREFERENCES, filter);
|
|
return rows.map((r) => ({ key: r.key, value: r.value, category: r.category }));
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Insights */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
async storeInsight(insight: NewInsight): Promise<Insight> {
|
|
const now = new Date();
|
|
const row = await this.storage.create<Record<string, unknown>>(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<Insight | null> {
|
|
const row = await this.storage.read<InsightRecord>(INSIGHTS, id);
|
|
if (!row) return null;
|
|
return toInsight(row);
|
|
}
|
|
|
|
async searchInsights(
|
|
userId: string,
|
|
query: string,
|
|
opts?: { limit?: number; embedding?: number[] },
|
|
): Promise<InsightSearchResult[]> {
|
|
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<InsightRecord>(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<boolean> {
|
|
return this.storage.delete(INSIGHTS, id);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Lifecycle */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
async close(): Promise<void> {
|
|
// 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,
|
|
};
|
|
}
|