UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

261 lines (260 loc) 10.1 kB
/** * MemoryRepository — Agent 语义记忆的仓储实现 * * 从 MemoryStore 提取的数据操作, * 使用 Drizzle 类型安全 API 操作 semantic_memories 表。 * embedding 已迁移到 JSON sidecar (MemoryEmbeddingStore)。 */ import { and, avg, count, desc, eq, isNull, or, sql } from 'drizzle-orm'; import { semanticMemories } from '../../infrastructure/database/drizzle/schema.js'; import { RepositoryBase } from '../base/RepositoryBase.js'; /* ═══ 常量 ═══ */ const MAX_MEMORIES = 500; const ARCHIVE_DAYS = 30; const FORGET_DAYS = 90; /* ═══ Repository 实现 ═══ */ export class MemoryRepositoryImpl extends RepositoryBase { constructor(drizzle) { super(drizzle, semanticMemories); } /* ─── CRUD ─── */ async findById(id) { const row = this.drizzle.select().from(this.table).where(eq(this.table.id, id)).limit(1).get(); return row ? this.#mapRow(row) : null; } async create(data) { const now = new Date().toISOString(); this.drizzle .insert(this.table) .values({ id: data.id, type: data.type ?? 'fact', content: (data.content || '').trim().substring(0, 500), source: data.source ?? 'bootstrap', importance: Math.max(1, Math.min(10, data.importance ?? 5)), accessCount: 0, lastAccessedAt: now, createdAt: now, updatedAt: now, expiresAt: data.expiresAt ?? null, relatedEntities: JSON.stringify(data.relatedEntities ?? []), relatedMemories: JSON.stringify([]), sourceDimension: data.sourceDimension ?? null, sourceEvidence: data.sourceEvidence ?? null, bootstrapSession: data.bootstrapSession ?? null, tags: JSON.stringify(data.tags ?? []), }) .run(); return (await this.findById(data.id)); } async delete(id) { const result = this.drizzle.delete(this.table).where(eq(this.table.id, id)).run(); return result.changes > 0; } /** 动态字段更新 */ async update(id, updates) { const existing = await this.findById(id); if (!existing) { return false; } const now = new Date().toISOString(); const setValues = { updatedAt: now }; if (updates.content !== undefined) { setValues.content = updates.content.substring(0, 500); } if (updates.importance !== undefined) { setValues.importance = Math.max(1, Math.min(10, updates.importance)); } if (updates.accessCount !== undefined) { setValues.accessCount = updates.accessCount; } if (updates.relatedEntities !== undefined) { setValues.relatedEntities = JSON.stringify(updates.relatedEntities); } if (updates.relatedMemories !== undefined) { setValues.relatedMemories = JSON.stringify(updates.relatedMemories); } if (updates.tags !== undefined) { setValues.tags = JSON.stringify(updates.tags); } this.drizzle.update(this.table).set(setValues).where(eq(this.table.id, id)).run(); return true; } /* ─── 访问计数 ─── */ /** 更新访问计数 */ async touchAccess(id) { this.drizzle .update(this.table) .set({ accessCount: sql `${this.table.accessCount} + 1`, lastAccessedAt: new Date().toISOString(), }) .where(eq(this.table.id, id)) .run(); } /* ─── 批量查询 ─── */ /** 获取所有活跃记忆 (未过期) */ async getAllActive(filters = {}) { const now = new Date().toISOString(); const conditions = [or(isNull(this.table.expiresAt), sql `${this.table.expiresAt} > ${now}`)]; if (filters.source) { conditions.push(eq(this.table.source, filters.source)); } if (filters.type) { conditions.push(eq(this.table.type, filters.type)); } const rows = this.drizzle .select() .from(this.table) .where(and(...conditions)) .orderBy(desc(this.table.updatedAt)) .all(); return rows.map((r) => this.#mapRow(r)); } /** 获取候选记忆 (用于相似度搜索) */ async getCandidates(type, limit = 50) { const now = new Date().toISOString(); const conditions = [or(isNull(this.table.expiresAt), sql `${this.table.expiresAt} > ${now}`)]; if (type) { conditions.push(eq(this.table.type, type)); } const rows = this.drizzle .select() .from(this.table) .where(and(...conditions)) .orderBy(desc(this.table.updatedAt)) .limit(limit) .all(); return rows.map((r) => this.#mapRow(r)); } /** 记忆总数 */ async size(filters = {}) { const condition = filters.source ? eq(this.table.source, filters.source) : undefined; const [row] = this.drizzle.select({ cnt: count() }).from(this.table).where(condition).all(); return row?.cnt ?? 0; } /* ─── 维护 ─── */ /** * 执行维护: 清理过期记忆 + 自然遗忘 + 重要度衰减 */ async compact() { const stats = { expired: 0, forgotten: 0, archived: 0, remaining: 0 }; const now = new Date().toISOString(); const nowMs = Date.now(); this.transaction(() => { // 清理过期 const expiredResult = this.drizzle .delete(this.table) .where(and(sql `${this.table.expiresAt} IS NOT NULL`, sql `${this.table.expiresAt} < ${now}`)) .run(); stats.expired = expiredResult.changes; // 自然遗忘: 90 天未访问 + importance < 7 const forgetThreshold = new Date(nowMs - FORGET_DAYS * 86400_000).toISOString(); const forgottenResult = this.drizzle .delete(this.table) .where(and(sql `${this.table.lastAccessedAt} < ${forgetThreshold}`, sql `${this.table.importance} < 7`)) .run(); stats.forgotten = forgottenResult.changes; // 30 天未访问 + importance < 3 → 衰减 const archiveThreshold = new Date(nowMs - ARCHIVE_DAYS * 86400_000).toISOString(); const archiveResult = this.drizzle .update(this.table) .set({ importance: sql `MAX(1, ${this.table.importance} - 1)`, }) .where(and(sql `${this.table.lastAccessedAt} < ${archiveThreshold}`, sql `${this.table.importance} < 3`)) .run(); stats.archived = archiveResult.changes; // 剩余数量 const [row] = this.drizzle.select({ cnt: count() }).from(this.table).all(); stats.remaining = row?.cnt ?? 0; }); return stats; } /** 容量控制 */ async enforceCapacity(maxMemories = MAX_MEMORIES) { const currentSize = await this.size(); if (currentSize <= maxMemories) { return 0; } const excess = currentSize - maxMemories; const result = this.drizzle .delete(this.table) .where(sql `${this.table.id} IN ( SELECT ${this.table.id} FROM ${this.table} ORDER BY ${this.table.importance} ASC, ${this.table.accessCount} ASC, ${this.table.updatedAt} ASC LIMIT ${excess} )`) .run(); return result.changes; } /* ─── 统计 ─── */ async getStats() { const [totalRow] = this.drizzle.select({ cnt: count() }).from(this.table).all(); const total = totalRow?.cnt ?? 0; const byTypeRows = this.drizzle .select({ type: this.table.type, cnt: count(), }) .from(this.table) .groupBy(this.table.type) .all(); const bySourceRows = this.drizzle .select({ source: this.table.source, cnt: count(), }) .from(this.table) .groupBy(this.table.source) .all(); const [avgRow] = this.drizzle .select({ avg: avg(this.table.importance) }) .from(this.table) .all(); const avgImportance = avgRow?.avg ? Math.round(Number(avgRow.avg) * 10) / 10 : 0; return { total, byType: Object.fromEntries(byTypeRows.map((r) => [r.type, r.cnt])), bySource: Object.fromEntries(bySourceRows.map((r) => [r.source, r.cnt])), avgImportance, }; } /** 清除所有 bootstrap 来源的记忆 */ async clearBootstrapMemories() { const result = this.drizzle.delete(this.table).where(eq(this.table.source, 'bootstrap')).run(); return result.changes; } /* ─── 内部辅助 ─── */ #mapRow(row) { return { id: row.id, type: row.type, content: row.content, source: row.source, importance: row.importance, accessCount: row.accessCount, lastAccessedAt: row.lastAccessedAt ?? null, createdAt: row.createdAt, updatedAt: row.updatedAt, expiresAt: row.expiresAt ?? null, relatedEntities: safeParseJSON(row.relatedEntities, []), relatedMemories: safeParseJSON(row.relatedMemories, []), sourceDimension: row.sourceDimension ?? null, sourceEvidence: row.sourceEvidence ?? null, bootstrapSession: row.bootstrapSession ?? null, tags: safeParseJSON(row.tags, []), }; } } /* ═══ 辅助函数 ═══ */ function safeParseJSON(str, fallback) { try { return str ? JSON.parse(str) : fallback; } catch { return fallback; } }