/** * Vector Memory - Semantic search wrapper for L4 self-evolution * * Provides vector-based semantic search over agent memories using OpenViking. * This enables finding conceptually similar memories rather than just keyword matches. * * Key capabilities: * - Semantic search: Find memories by meaning, not just keywords * - Relevance scoring: Get similarity scores for search results * - Context-aware: Search at different context levels (L0/L1/L2) * * Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md ยง6.4.2 */ import { getVikingClient, type VikingHttpClient } from './viking-client'; import { getMemoryManager, type MemoryEntry, type MemoryType } from './agent-memory'; // === Types === export interface VectorSearchResult { memory: MemoryEntry; score: number; uri: string; highlights?: string[]; } export interface VectorSearchOptions { topK?: number; // Number of results to return (default: 10) minScore?: number; // Minimum relevance score (default: 0.5) types?: MemoryType[]; // Filter by memory types agentId?: string; // Filter by agent level?: 'L0' | 'L1' | 'L2'; // Context level to search } export interface VectorEmbedding { id: string; vector: number[]; dimension: number; model: string; } export interface VectorMemoryConfig { enabled: boolean; defaultTopK: number; defaultMinScore: number; defaultLevel: 'L0' | 'L1' | 'L2'; embeddingModel: string; cacheEmbeddings: boolean; } // === Default Config === export const DEFAULT_VECTOR_CONFIG: VectorMemoryConfig = { enabled: true, defaultTopK: 10, defaultMinScore: 0.3, defaultLevel: 'L1', embeddingModel: 'text-embedding-ada-002', cacheEmbeddings: true, }; // === Vector Memory Service === export class VectorMemoryService { private config: VectorMemoryConfig; private vikingClient: VikingHttpClient | null = null; private embeddingCache: Map = new Map(); constructor(config?: Partial) { this.config = { ...DEFAULT_VECTOR_CONFIG, ...config }; this.initializeClient(); } private async initializeClient(): Promise { try { this.vikingClient = getVikingClient(); } catch (error) { console.warn('[VectorMemory] Failed to initialize Viking client:', error); } } // === Semantic Search === /** * Perform semantic search over memories. * Uses OpenViking's built-in vector search capabilities. */ async semanticSearch( query: string, options?: VectorSearchOptions ): Promise { if (!this.config.enabled) { console.warn('[VectorMemory] Semantic search is disabled'); return []; } if (!this.vikingClient) { await this.initializeClient(); if (!this.vikingClient) { console.warn('[VectorMemory] Viking client not available'); return []; } } try { const results = await this.vikingClient.find(query, { limit: options?.topK ?? this.config.defaultTopK, minScore: options?.minScore ?? this.config.defaultMinScore, level: options?.level ?? this.config.defaultLevel, scope: options?.agentId ? `memories/${options.agentId}` : undefined, }); // Convert FindResult to VectorSearchResult const searchResults: VectorSearchResult[] = []; for (const result of results) { // Convert Viking result to MemoryEntry format const memory: MemoryEntry = { id: this.extractMemoryId(result.uri), agentId: options?.agentId ?? 'unknown', content: result.content, type: this.inferMemoryType(result.uri), importance: Math.round((1 - result.score) * 10), // Invert score to importance createdAt: new Date().toISOString(), source: 'auto', tags: Array.isArray((result.metadata as Record)?.tags) ? (result.metadata as Record).tags as string[] : [], lastAccessedAt: new Date().toISOString(), accessCount: 0, }; searchResults.push({ memory, score: result.score, uri: result.uri, highlights: Array.isArray((result.metadata as Record)?.highlights) ? (result.metadata as Record).highlights as string[] : undefined, }); } // Apply type filter if specified if (options?.types && options.types.length > 0) { return searchResults.filter(r => options.types!.includes(r.memory.type)); } return searchResults; } catch (error) { console.error('[VectorMemory] Semantic search failed:', error); return []; } } /** * Find similar memories to a given memory. */ async findSimilar( memoryId: string, options?: Omit ): Promise { // Get the memory content first const memoryManager = getMemoryManager(); const memories = await memoryManager.getAll(options?.agentId ?? 'default'); const memory = memories.find((m: MemoryEntry) => m.id === memoryId); if (!memory) { console.warn(`[VectorMemory] Memory not found: ${memoryId}`); return []; } // Use the memory content as query for semantic search const results = await this.semanticSearch(memory.content, { ...options, topK: (options?.topK ?? 10) + 1, // +1 to account for the memory itself }); // Filter out the original memory from results return results.filter(r => r.memory.id !== memoryId); } /** * Find memories related to a topic/concept. */ async findByConcept( concept: string, options?: VectorSearchOptions ): Promise { return this.semanticSearch(concept, options); } /** * Cluster memories by semantic similarity. * Returns groups of related memories. */ async clusterMemories( agentId: string, clusterCount: number = 5 ): Promise { const memoryManager = getMemoryManager(); const memories = await memoryManager.getAll(agentId); if (memories.length === 0) { return []; } // Simple clustering: use each memory as a seed and find similar ones const clusters: VectorSearchResult[][] = []; const usedIds = new Set(); for (const memory of memories) { if (usedIds.has(memory.id)) continue; const similar = await this.findSimilar(memory.id, { agentId, topK: clusterCount }); if (similar.length > 0) { const cluster: VectorSearchResult[] = [ { memory, score: 1.0, uri: `memory://${memory.id}` }, ...similar.filter(r => !usedIds.has(r.memory.id)), ]; cluster.forEach(r => usedIds.add(r.memory.id)); clusters.push(cluster); if (clusters.length >= clusterCount) break; } } return clusters; } // === Embedding Operations === /** * Get or compute embedding for a text. * Note: OpenViking handles embeddings internally, this is for advanced use. */ async getEmbedding(text: string): Promise { if (!this.config.enabled) return null; // Check cache first const cacheKey = this.hashText(text); if (this.config.cacheEmbeddings && this.embeddingCache.has(cacheKey)) { return this.embeddingCache.get(cacheKey)!; } // OpenViking handles embeddings internally via /api/find // This method is provided for future extensibility console.warn('[VectorMemory] Direct embedding computation not available - OpenViking handles this internally'); return null; } /** * Compute similarity between two texts. */ async computeSimilarity(text1: string, text2: string): Promise { if (!this.config.enabled || !this.vikingClient) return 0; try { // Use OpenViking to find text1, then check if text2 is in results const results = await this.vikingClient.find(text1, { limit: 20 }); // If we find text2 in results, return its score for (const result of results) { if (result.content.includes(text2) || text2.includes(result.content)) { return result.score; } } // Otherwise, return 0 (no similarity found) return 0; } catch { return 0; } } // === Utility Methods === /** * Check if vector search is available. */ async isAvailable(): Promise { if (!this.config.enabled) return false; if (!this.vikingClient) { await this.initializeClient(); } return this.vikingClient?.isAvailable() ?? false; } /** * Get current configuration. */ getConfig(): VectorMemoryConfig { return { ...this.config }; } /** * Update configuration. */ updateConfig(updates: Partial): void { this.config = { ...this.config, ...updates }; } /** * Clear embedding cache. */ clearCache(): void { this.embeddingCache.clear(); } // === Private Helpers === private extractMemoryId(uri: string): string { // Extract memory ID from Viking URI // Format: memories/agent-id/memory-id or similar const parts = uri.split('/'); return parts[parts.length - 1] || uri; } private inferMemoryType(uri: string): MemoryType { // Infer memory type from URI or metadata if (uri.includes('preference')) return 'preference'; if (uri.includes('fact')) return 'fact'; if (uri.includes('task')) return 'task'; if (uri.includes('lesson')) return 'lesson'; return 'fact'; // Default } private hashText(text: string): string { // Simple hash for cache key let hash = 0; for (let i = 0; i < text.length; i++) { const char = text.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return hash.toString(16); } } // === Singleton === let _instance: VectorMemoryService | null = null; export function getVectorMemory(): VectorMemoryService { if (!_instance) { _instance = new VectorMemoryService(); } return _instance; } export function resetVectorMemory(): void { _instance = null; } // === Helper Functions === /** * Quick semantic search helper. */ export async function semanticSearch( query: string, options?: VectorSearchOptions ): Promise { return getVectorMemory().semanticSearch(query, options); } /** * Find similar memories helper. */ export async function findSimilarMemories( memoryId: string, agentId?: string ): Promise { return getVectorMemory().findSimilar(memoryId, { agentId }); } /** * Check if vector search is available. */ export async function isVectorSearchAvailable(): Promise { return getVectorMemory().isAvailable(); }