feat(memory): implement keyword search adapter — no vector dependency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mosaic/db": "workspace:*",
|
"@mosaic/db": "workspace:*",
|
||||||
|
"@mosaic/storage": "workspace:*",
|
||||||
"@mosaic/types": "workspace:*",
|
"@mosaic/types": "workspace:*",
|
||||||
"drizzle-orm": "^0.45.1"
|
"drizzle-orm": "^0.45.1"
|
||||||
},
|
},
|
||||||
|
|||||||
298
packages/memory/src/adapters/keyword.test.ts
Normal file
298
packages/memory/src/adapters/keyword.test.ts
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import type { StorageAdapter } from '@mosaic/storage';
|
||||||
|
import { KeywordAdapter } from './keyword.js';
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* In-memory mock StorageAdapter */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
function createMockStorage(): StorageAdapter {
|
||||||
|
const collections = new Map<string, Map<string, Record<string, unknown>>>();
|
||||||
|
let idCounter = 0;
|
||||||
|
|
||||||
|
function getCollection(name: string): Map<string, Record<string, unknown>> {
|
||||||
|
if (!collections.has(name)) collections.set(name, new Map());
|
||||||
|
return collections.get(name)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapter: StorageAdapter = {
|
||||||
|
name: 'mock',
|
||||||
|
|
||||||
|
async create<T extends Record<string, unknown>>(
|
||||||
|
collection: string,
|
||||||
|
data: T,
|
||||||
|
): Promise<T & { id: string }> {
|
||||||
|
const id = String(++idCounter);
|
||||||
|
const record = { ...data, id };
|
||||||
|
getCollection(collection).set(id, record);
|
||||||
|
return record as T & { id: string };
|
||||||
|
},
|
||||||
|
|
||||||
|
async read<T extends Record<string, unknown>>(
|
||||||
|
collection: string,
|
||||||
|
id: string,
|
||||||
|
): Promise<T | null> {
|
||||||
|
const record = getCollection(collection).get(id);
|
||||||
|
return (record as T) ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(collection: string, id: string, data: Record<string, unknown>): Promise<boolean> {
|
||||||
|
const col = getCollection(collection);
|
||||||
|
const existing = col.get(id);
|
||||||
|
if (!existing) return false;
|
||||||
|
col.set(id, { ...existing, ...data });
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(collection: string, id: string): Promise<boolean> {
|
||||||
|
return getCollection(collection).delete(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
async find<T extends Record<string, unknown>>(
|
||||||
|
collection: string,
|
||||||
|
filter?: Record<string, unknown>,
|
||||||
|
): Promise<T[]> {
|
||||||
|
const col = getCollection(collection);
|
||||||
|
const results: T[] = [];
|
||||||
|
for (const record of col.values()) {
|
||||||
|
if (filter && !matchesFilter(record, filter)) continue;
|
||||||
|
results.push(record as T);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
async findOne<T extends Record<string, unknown>>(
|
||||||
|
collection: string,
|
||||||
|
filter: Record<string, unknown>,
|
||||||
|
): Promise<T | null> {
|
||||||
|
const col = getCollection(collection);
|
||||||
|
for (const record of col.values()) {
|
||||||
|
if (matchesFilter(record, filter)) return record as T;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async count(collection: string, filter?: Record<string, unknown>): Promise<number> {
|
||||||
|
const rows = await adapter.find(collection, filter);
|
||||||
|
return rows.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
async transaction<T>(fn: (tx: StorageAdapter) => Promise<T>): Promise<T> {
|
||||||
|
return fn(adapter);
|
||||||
|
},
|
||||||
|
|
||||||
|
async migrate(): Promise<void> {},
|
||||||
|
async close(): Promise<void> {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesFilter(record: Record<string, unknown>, filter: Record<string, unknown>): boolean {
|
||||||
|
for (const [key, value] of Object.entries(filter)) {
|
||||||
|
if (record[key] !== value) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Tests */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
describe('KeywordAdapter', () => {
|
||||||
|
let adapter: KeywordAdapter;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
adapter = new KeywordAdapter({ type: 'keyword', storage: createMockStorage() });
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---- Preferences ---- */
|
||||||
|
|
||||||
|
describe('preferences', () => {
|
||||||
|
it('should set and get a preference', async () => {
|
||||||
|
await adapter.setPreference('u1', 'theme', 'dark');
|
||||||
|
const value = await adapter.getPreference('u1', 'theme');
|
||||||
|
expect(value).toBe('dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for missing preference', async () => {
|
||||||
|
const value = await adapter.getPreference('u1', 'nonexistent');
|
||||||
|
expect(value).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should upsert an existing preference', async () => {
|
||||||
|
await adapter.setPreference('u1', 'theme', 'dark');
|
||||||
|
await adapter.setPreference('u1', 'theme', 'light');
|
||||||
|
const value = await adapter.getPreference('u1', 'theme');
|
||||||
|
expect(value).toBe('light');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete a preference', async () => {
|
||||||
|
await adapter.setPreference('u1', 'theme', 'dark');
|
||||||
|
const deleted = await adapter.deletePreference('u1', 'theme');
|
||||||
|
expect(deleted).toBe(true);
|
||||||
|
const value = await adapter.getPreference('u1', 'theme');
|
||||||
|
expect(value).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when deleting nonexistent preference', async () => {
|
||||||
|
const deleted = await adapter.deletePreference('u1', 'nope');
|
||||||
|
expect(deleted).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list preferences by userId', async () => {
|
||||||
|
await adapter.setPreference('u1', 'theme', 'dark', 'appearance');
|
||||||
|
await adapter.setPreference('u1', 'lang', 'en', 'locale');
|
||||||
|
await adapter.setPreference('u2', 'theme', 'light', 'appearance');
|
||||||
|
|
||||||
|
const prefs = await adapter.listPreferences('u1');
|
||||||
|
expect(prefs).toHaveLength(2);
|
||||||
|
expect(prefs.map((p) => p.key).sort()).toEqual(['lang', 'theme']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter preferences by category', async () => {
|
||||||
|
await adapter.setPreference('u1', 'theme', 'dark', 'appearance');
|
||||||
|
await adapter.setPreference('u1', 'lang', 'en', 'locale');
|
||||||
|
|
||||||
|
const prefs = await adapter.listPreferences('u1', 'appearance');
|
||||||
|
expect(prefs).toHaveLength(1);
|
||||||
|
expect(prefs[0]!.key).toBe('theme');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---- Insights ---- */
|
||||||
|
|
||||||
|
describe('insights', () => {
|
||||||
|
it('should store and retrieve an insight', async () => {
|
||||||
|
const insight = await adapter.storeInsight({
|
||||||
|
userId: 'u1',
|
||||||
|
content: 'TypeScript is great for type safety',
|
||||||
|
source: 'chat',
|
||||||
|
category: 'technical',
|
||||||
|
relevanceScore: 0.9,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(insight.id).toBeDefined();
|
||||||
|
expect(insight.content).toBe('TypeScript is great for type safety');
|
||||||
|
|
||||||
|
const fetched = await adapter.getInsight(insight.id);
|
||||||
|
expect(fetched).not.toBeNull();
|
||||||
|
expect(fetched!.content).toBe('TypeScript is great for type safety');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for missing insight', async () => {
|
||||||
|
const result = await adapter.getInsight('nonexistent');
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete an insight', async () => {
|
||||||
|
const insight = await adapter.storeInsight({
|
||||||
|
userId: 'u1',
|
||||||
|
content: 'test',
|
||||||
|
source: 'chat',
|
||||||
|
category: 'general',
|
||||||
|
relevanceScore: 0.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleted = await adapter.deleteInsight(insight.id);
|
||||||
|
expect(deleted).toBe(true);
|
||||||
|
|
||||||
|
const fetched = await adapter.getInsight(insight.id);
|
||||||
|
expect(fetched).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---- Keyword Search ---- */
|
||||||
|
|
||||||
|
describe('searchInsights', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await adapter.storeInsight({
|
||||||
|
userId: 'u1',
|
||||||
|
content: 'TypeScript provides excellent type safety for JavaScript projects',
|
||||||
|
source: 'chat',
|
||||||
|
category: 'technical',
|
||||||
|
relevanceScore: 0.9,
|
||||||
|
});
|
||||||
|
await adapter.storeInsight({
|
||||||
|
userId: 'u1',
|
||||||
|
content: 'React hooks simplify state management in components',
|
||||||
|
source: 'chat',
|
||||||
|
category: 'technical',
|
||||||
|
relevanceScore: 0.8,
|
||||||
|
});
|
||||||
|
await adapter.storeInsight({
|
||||||
|
userId: 'u1',
|
||||||
|
content: 'TypeScript and React work great together for type safe components',
|
||||||
|
source: 'chat',
|
||||||
|
category: 'technical',
|
||||||
|
relevanceScore: 0.85,
|
||||||
|
});
|
||||||
|
await adapter.storeInsight({
|
||||||
|
userId: 'u2',
|
||||||
|
content: 'TypeScript is popular',
|
||||||
|
source: 'chat',
|
||||||
|
category: 'general',
|
||||||
|
relevanceScore: 0.5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find insights by exact keyword', async () => {
|
||||||
|
const results = await adapter.searchInsights('u1', 'hooks');
|
||||||
|
expect(results).toHaveLength(1);
|
||||||
|
expect(results[0]!.content).toContain('hooks');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be case-insensitive', async () => {
|
||||||
|
const results = await adapter.searchInsights('u1', 'TYPESCRIPT');
|
||||||
|
expect(results.length).toBeGreaterThanOrEqual(1);
|
||||||
|
for (const r of results) {
|
||||||
|
expect(r.content.toLowerCase()).toContain('typescript');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rank multi-word matches higher', async () => {
|
||||||
|
const results = await adapter.searchInsights('u1', 'TypeScript React');
|
||||||
|
// The insight mentioning both "TypeScript" and "React" should rank first (score=2)
|
||||||
|
expect(results[0]!.score).toBe(2);
|
||||||
|
expect(results[0]!.content).toContain('TypeScript');
|
||||||
|
expect(results[0]!.content).toContain('React');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty for no matches', async () => {
|
||||||
|
const results = await adapter.searchInsights('u1', 'python django');
|
||||||
|
expect(results).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by userId', async () => {
|
||||||
|
const results = await adapter.searchInsights('u2', 'TypeScript');
|
||||||
|
expect(results).toHaveLength(1);
|
||||||
|
expect(results[0]!.content).toBe('TypeScript is popular');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect limit option', async () => {
|
||||||
|
const results = await adapter.searchInsights('u1', 'TypeScript', { limit: 1 });
|
||||||
|
expect(results).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty for empty query', async () => {
|
||||||
|
const results = await adapter.searchInsights('u1', ' ');
|
||||||
|
expect(results).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---- Lifecycle ---- */
|
||||||
|
|
||||||
|
describe('lifecycle', () => {
|
||||||
|
it('should have name "keyword"', () => {
|
||||||
|
expect(adapter.name).toBe('keyword');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have null embedder', () => {
|
||||||
|
expect(adapter.embedder).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close without error', async () => {
|
||||||
|
await expect(adapter.close()).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
195
packages/memory/src/adapters/keyword.ts
Normal file
195
packages/memory/src/adapters/keyword.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -22,12 +22,18 @@ export type {
|
|||||||
} from './types.js';
|
} from './types.js';
|
||||||
export { createMemoryAdapter, registerMemoryAdapter } from './factory.js';
|
export { createMemoryAdapter, registerMemoryAdapter } from './factory.js';
|
||||||
export { PgVectorAdapter } from './adapters/pgvector.js';
|
export { PgVectorAdapter } from './adapters/pgvector.js';
|
||||||
|
export { KeywordAdapter } from './adapters/keyword.js';
|
||||||
|
|
||||||
// Auto-register pgvector adapter at module load time
|
// Auto-register adapters at module load time
|
||||||
import { registerMemoryAdapter } from './factory.js';
|
import { registerMemoryAdapter } from './factory.js';
|
||||||
import { PgVectorAdapter } from './adapters/pgvector.js';
|
import { PgVectorAdapter } from './adapters/pgvector.js';
|
||||||
|
import { KeywordAdapter } from './adapters/keyword.js';
|
||||||
import type { MemoryConfig } from './types.js';
|
import type { MemoryConfig } from './types.js';
|
||||||
|
|
||||||
registerMemoryAdapter('pgvector', (config: MemoryConfig) => {
|
registerMemoryAdapter('pgvector', (config: MemoryConfig) => {
|
||||||
return new PgVectorAdapter(config as Extract<MemoryConfig, { type: 'pgvector' }>);
|
return new PgVectorAdapter(config as Extract<MemoryConfig, { type: 'pgvector' }>);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerMemoryAdapter('keyword', (config: MemoryConfig) => {
|
||||||
|
return new KeywordAdapter(config as Extract<MemoryConfig, { type: 'keyword' }>);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type { EmbeddingProvider, VectorSearchResult } from './vector-store.js';
|
export type { EmbeddingProvider, VectorSearchResult } from './vector-store.js';
|
||||||
import type { EmbeddingProvider } from './vector-store.js';
|
import type { EmbeddingProvider } from './vector-store.js';
|
||||||
|
import type { StorageAdapter } from '@mosaic/storage';
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Insight types (adapter-level, decoupled from Drizzle schema) */
|
/* Insight types (adapter-level, decoupled from Drizzle schema) */
|
||||||
@@ -69,4 +70,4 @@ export interface MemoryAdapter {
|
|||||||
export type MemoryConfig =
|
export type MemoryConfig =
|
||||||
| { type: 'pgvector'; embedder?: EmbeddingProvider }
|
| { type: 'pgvector'; embedder?: EmbeddingProvider }
|
||||||
| { type: 'sqlite-vec'; embedder?: EmbeddingProvider }
|
| { type: 'sqlite-vec'; embedder?: EmbeddingProvider }
|
||||||
| { type: 'keyword' };
|
| { type: 'keyword'; storage: StorageAdapter };
|
||||||
|
|||||||
54
pnpm-lock.yaml
generated
54
pnpm-lock.yaml
generated
@@ -456,6 +456,9 @@ importers:
|
|||||||
'@mosaic/db':
|
'@mosaic/db':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../db
|
version: link:../db
|
||||||
|
'@mosaic/storage':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../storage
|
||||||
'@mosaic/types':
|
'@mosaic/types':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../types
|
version: link:../types
|
||||||
@@ -634,10 +637,10 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@mariozechner/pi-agent-core':
|
'@mariozechner/pi-agent-core':
|
||||||
specifier: ^0.63.1
|
specifier: ^0.63.1
|
||||||
version: 0.63.2(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@3.25.76)
|
version: 0.63.2(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
|
||||||
'@mariozechner/pi-ai':
|
'@mariozechner/pi-ai':
|
||||||
specifier: ^0.63.1
|
specifier: ^0.63.1
|
||||||
version: 0.63.2(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@3.25.76)
|
version: 0.63.2(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
|
||||||
'@sinclair/typebox':
|
'@sinclair/typebox':
|
||||||
specifier: ^0.34.41
|
specifier: ^0.34.41
|
||||||
version: 0.34.48
|
version: 0.34.48
|
||||||
@@ -6999,12 +7002,6 @@ snapshots:
|
|||||||
'@jridgewell/gen-mapping': 0.3.13
|
'@jridgewell/gen-mapping': 0.3.13
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
|
||||||
'@anthropic-ai/sdk@0.73.0(zod@3.25.76)':
|
|
||||||
dependencies:
|
|
||||||
json-schema-to-ts: 3.1.1
|
|
||||||
optionalDependencies:
|
|
||||||
zod: 3.25.76
|
|
||||||
|
|
||||||
'@anthropic-ai/sdk@0.73.0(zod@4.3.6)':
|
'@anthropic-ai/sdk@0.73.0(zod@4.3.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
json-schema-to-ts: 3.1.1
|
json-schema-to-ts: 3.1.1
|
||||||
@@ -8589,18 +8586,6 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@mariozechner/pi-agent-core@0.63.2(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@3.25.76)':
|
|
||||||
dependencies:
|
|
||||||
'@mariozechner/pi-ai': 0.63.2(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@3.25.76)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@modelcontextprotocol/sdk'
|
|
||||||
- aws-crt
|
|
||||||
- bufferutil
|
|
||||||
- supports-color
|
|
||||||
- utf-8-validate
|
|
||||||
- ws
|
|
||||||
- zod
|
|
||||||
|
|
||||||
'@mariozechner/pi-agent-core@0.63.2(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
|
'@mariozechner/pi-agent-core@0.63.2(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mariozechner/pi-ai': 0.63.2(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
|
'@mariozechner/pi-ai': 0.63.2(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
|
||||||
@@ -8661,30 +8646,6 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@mariozechner/pi-ai@0.63.2(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@3.25.76)':
|
|
||||||
dependencies:
|
|
||||||
'@anthropic-ai/sdk': 0.73.0(zod@3.25.76)
|
|
||||||
'@aws-sdk/client-bedrock-runtime': 3.1008.0
|
|
||||||
'@google/genai': 1.45.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))
|
|
||||||
'@mistralai/mistralai': 1.14.1
|
|
||||||
'@sinclair/typebox': 0.34.48
|
|
||||||
ajv: 8.18.0
|
|
||||||
ajv-formats: 3.0.1(ajv@8.18.0)
|
|
||||||
chalk: 5.6.2
|
|
||||||
openai: 6.26.0(ws@8.20.0)(zod@3.25.76)
|
|
||||||
partial-json: 0.1.7
|
|
||||||
proxy-agent: 6.5.0
|
|
||||||
undici: 7.24.3
|
|
||||||
zod-to-json-schema: 3.25.1(zod@3.25.76)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@modelcontextprotocol/sdk'
|
|
||||||
- aws-crt
|
|
||||||
- bufferutil
|
|
||||||
- supports-color
|
|
||||||
- utf-8-validate
|
|
||||||
- ws
|
|
||||||
- zod
|
|
||||||
|
|
||||||
'@mariozechner/pi-ai@0.63.2(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
|
'@mariozechner/pi-ai@0.63.2(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.73.0(zod@4.3.6)
|
'@anthropic-ai/sdk': 0.73.0(zod@4.3.6)
|
||||||
@@ -12903,11 +12864,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-function: 5.0.1
|
mimic-function: 5.0.1
|
||||||
|
|
||||||
openai@6.26.0(ws@8.20.0)(zod@3.25.76):
|
|
||||||
optionalDependencies:
|
|
||||||
ws: 8.20.0
|
|
||||||
zod: 3.25.76
|
|
||||||
|
|
||||||
openai@6.26.0(ws@8.20.0)(zod@4.3.6):
|
openai@6.26.0(ws@8.20.0)(zod@4.3.6):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
ws: 8.20.0
|
ws: 8.20.0
|
||||||
|
|||||||
Reference in New Issue
Block a user