Skip to main content
Glama
SemanticMemory.ts13.8 kB
/** * Semantic Memory System * * Implements embedding-based similarity search for semantic memories. * Handles storage, retrieval, and relationship management of concepts. */ import { ComponentStatus, ISemanticMemory } from "../interfaces/cognitive.js"; import { Concept, Relation } from "../types/core.js"; export interface SemanticMemoryConfig { capacity: number; embedding_dim: number; similarity_threshold: number; activation_decay: number; relation_strength_threshold: number; max_relations_per_concept: number; } export class SemanticMemory implements ISemanticMemory { private concepts: Map<string, Concept> = new Map(); private relations: Map<string, Relation> = new Map(); private embeddingIndex: Map<string, number[]> = new Map(); private activationIndex: Map<string, number> = new Map(); private config: SemanticMemoryConfig; private initialized: boolean = false; private lastActivity: number = 0; constructor(config?: Partial<SemanticMemoryConfig>) { this.config = { capacity: 50000, embedding_dim: 768, similarity_threshold: 0.7, activation_decay: 0.05, relation_strength_threshold: 0.3, max_relations_per_concept: 20, ...config, }; } async initialize(config?: Partial<SemanticMemoryConfig>): Promise<void> { if (config) { this.config = { ...this.config, ...config }; } this.concepts.clear(); this.relations.clear(); this.embeddingIndex.clear(); this.activationIndex.clear(); this.initialized = true; this.lastActivity = Date.now(); } async process(input: unknown): Promise<unknown> { // Generic process method for CognitiveComponent interface const inputObj = input as { concept?: unknown }; if (typeof input === "object" && input !== null && inputObj?.concept) { return this.store(inputObj.concept as Concept); } throw new Error("Invalid input for SemanticMemory.process()"); } reset(): void { this.concepts.clear(); this.relations.clear(); this.embeddingIndex.clear(); this.activationIndex.clear(); this.lastActivity = Date.now(); } getStatus(): ComponentStatus { return { name: "SemanticMemory", initialized: this.initialized, active: this.concepts.size > 0, last_activity: this.lastActivity, }; } /** * Store a concept in semantic memory */ store(concept: Concept): string { this.lastActivity = Date.now(); // Generate ID if not provided const conceptId = concept.id && concept.id.length > 0 ? concept.id : this.generateConceptId(concept.content); // Generate embedding if not provided const embedding = concept.embedding ?? this.generateEmbedding(concept.content); // Store the concept const storedConcept: Concept = { ...concept, id: conceptId, embedding, activation: concept.activation ?? 1.0, last_accessed: Date.now(), relations: concept.relations ?? [], }; this.concepts.set(conceptId, storedConcept); this.embeddingIndex.set(conceptId, embedding); this.activationIndex.set(conceptId, storedConcept.activation); // Apply capacity management after storing if (this.concepts.size > this.config.capacity) { this.pruneLeastActiveConcepts(); } return conceptId; } /** * Retrieve concepts based on similarity to cue */ retrieve( cue: string, threshold: number = this.config.similarity_threshold ): Concept[] { this.lastActivity = Date.now(); const cueEmbedding = this.generateEmbedding(cue); const matches: Array<{ concept: Concept; similarity: number }> = []; // Compute similarity with all concepts for (const [id, concept] of this.concepts) { const embedding = this.embeddingIndex.get(id); if (!embedding) continue; const similarity = this.computeCosineSimilarity(cueEmbedding, embedding); if (similarity >= threshold) { matches.push({ concept, similarity }); // Update activation for retrieved concepts this.updateActivation(id, 0.1); } } // Sort by similarity and return concepts return matches .sort((a, b) => b.similarity - a.similarity) .map((match) => match.concept); } /** * Add a relation between two concepts */ addRelation(from: string, to: string, type: string, strength: number): void { this.lastActivity = Date.now(); // Validate concepts exist if (!this.concepts.has(from) || !this.concepts.has(to)) { throw new Error(`Cannot create relation: concept(s) not found`); } // Check strength threshold if (strength < this.config.relation_strength_threshold) { return; } const relationId = `${from}-${type}-${to}`; const relation: Relation = { from, to, type, strength }; this.relations.set(relationId, relation); // Update concept relations this.addRelationToConcept(from, relationId); this.addRelationToConcept(to, relationId); } /** * Get concepts related to a given concept */ getRelated(conceptId: string): Concept[] { const concept = this.concepts.get(conceptId); if (!concept) return []; const relatedConcepts: Concept[] = []; for (const relationId of concept.relations) { const relation = this.relations.get(relationId); if (!relation) continue; // Get the other concept in the relation const otherConceptId = relation.from === conceptId ? relation.to : relation.from; const otherConcept = this.concepts.get(otherConceptId); if (otherConcept) { relatedConcepts.push(otherConcept); } } // Sort by activation level return relatedConcepts.sort((a, b) => b.activation - a.activation); } /** * Update activation level of a concept */ updateActivation(conceptId: string, delta: number): void { const concept = this.concepts.get(conceptId); if (!concept) return; const newActivation = Math.max(0, Math.min(1, concept.activation + delta)); const updatedConcept: Concept = { ...concept, activation: newActivation, last_accessed: Date.now(), }; this.concepts.set(conceptId, updatedConcept); this.activationIndex.set(conceptId, newActivation); } /** * Apply activation decay to all concepts */ applyDecay(): void { const currentTime = Date.now(); for (const [id, concept] of this.concepts) { const timeSinceAccess = (currentTime - concept.last_accessed) / (1000 * 60 * 60); // hours const decayAmount = this.config.activation_decay * timeSinceAccess; const newActivation = Math.max(0, concept.activation - decayAmount); // Only update if there's actual decay if (newActivation !== concept.activation) { this.updateActivation(id, newActivation - concept.activation); } } } /** * Find concepts by content similarity */ findSimilarConcepts(conceptId: string, maxResults: number = 10): Concept[] { const concept = this.concepts.get(conceptId); if (!concept) return []; const embedding = this.embeddingIndex.get(conceptId); if (!embedding) return []; const similarities: Array<{ concept: Concept; similarity: number }> = []; for (const [id, otherConcept] of this.concepts) { if (id === conceptId) continue; const otherEmbedding = this.embeddingIndex.get(id); if (!otherEmbedding) continue; const similarity = this.computeCosineSimilarity( embedding, otherEmbedding ); similarities.push({ concept: otherConcept, similarity }); } return similarities .sort((a, b) => b.similarity - a.similarity) .slice(0, maxResults) .map((item) => item.concept); } /** * Get concept by ID */ getConcept(conceptId: string): Concept | undefined { return this.concepts.get(conceptId); } /** * Get all concepts with activation above threshold */ getActiveConcepts(threshold: number = 0.1): Concept[] { return Array.from(this.concepts.values()) .filter((concept) => concept.activation >= threshold) .sort((a, b) => b.activation - a.activation); } /** * Get relations of a specific type */ getRelationsByType(type: string): Relation[] { return Array.from(this.relations.values()).filter( (relation) => relation.type === type ); } // Private helper methods private generateConceptId(content: unknown): string { const contentStr = JSON.stringify(content); let hash = 0; for (let i = 0; i < contentStr.length; i++) { const char = contentStr.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } return `concept_${Math.abs(hash).toString(16)}`; } private generateEmbedding(content: unknown): number[] { // Simple embedding generation (in production, use a proper embedding model) const contentStr = JSON.stringify(content).toLowerCase(); const embedding = new Array(this.config.embedding_dim).fill(0); // Hash-based embedding generation for (let i = 0; i < contentStr.length; i++) { const char = contentStr.charCodeAt(i); const index = char % this.config.embedding_dim; embedding[index] += Math.sin(char * 0.1) * 0.1; } // Normalize the embedding const magnitude = Math.sqrt( embedding.reduce((sum, val) => sum + val * val, 0) ); if (magnitude > 0) { for (let i = 0; i < embedding.length; i++) { embedding[i] /= magnitude; } } return embedding; } private computeCosineSimilarity(a: number[], b: number[]): number { if (a.length !== b.length) return 0; let dotProduct = 0; let magnitudeA = 0; let magnitudeB = 0; for (let i = 0; i < a.length; i++) { dotProduct += a[i] * b[i]; magnitudeA += a[i] * a[i]; magnitudeB += b[i] * b[i]; } magnitudeA = Math.sqrt(magnitudeA); magnitudeB = Math.sqrt(magnitudeB); if (magnitudeA === 0 || magnitudeB === 0) return 0; return dotProduct / (magnitudeA * magnitudeB); } private addRelationToConcept(conceptId: string, relationId: string): void { const concept = this.concepts.get(conceptId); if (!concept) return; // Check if we've reached the maximum relations limit if (concept.relations.length >= this.config.max_relations_per_concept) { // Remove the weakest relation this.removeWeakestRelation(conceptId); } const updatedConcept: Concept = { ...concept, relations: [...concept.relations, relationId], }; this.concepts.set(conceptId, updatedConcept); } private removeWeakestRelation(conceptId: string): void { const concept = this.concepts.get(conceptId); if (!concept || concept.relations.length === 0) return; let weakestRelationId = ""; let weakestStrength = Infinity; for (const relationId of concept.relations) { const relation = this.relations.get(relationId); if (relation && relation.strength < weakestStrength) { weakestStrength = relation.strength; weakestRelationId = relationId; } } if (weakestRelationId) { // Remove from concept const updatedRelations = concept.relations.filter( (id) => id !== weakestRelationId ); const updatedConcept: Concept = { ...concept, relations: updatedRelations, }; this.concepts.set(conceptId, updatedConcept); // Remove the relation entirely this.relations.delete(weakestRelationId); } } private pruneLeastActiveConcepts(): void { // Remove concepts until we're at capacity const conceptEntries = Array.from(this.concepts.entries()); const sortedByActivation = conceptEntries.sort( (a, b) => a[1].activation - b[1].activation ); const toRemove = this.concepts.size - this.config.capacity; for (let i = 0; i < toRemove && i < sortedByActivation.length; i++) { const [conceptId] = sortedByActivation[i]; this.removeConcept(conceptId); } } private removeConcept(conceptId: string): void { const concept = this.concepts.get(conceptId); if (!concept) return; // Remove all relations involving this concept for (const relationId of concept.relations) { this.relations.delete(relationId); } // Remove from all indexes this.concepts.delete(conceptId); this.embeddingIndex.delete(conceptId); this.activationIndex.delete(conceptId); // Remove references from other concepts for (const [id, otherConcept] of this.concepts) { const updatedRelations = otherConcept.relations.filter((relationId) => { const relation = this.relations.get(relationId); return ( relation && relation.from !== conceptId && relation.to !== conceptId ); }); if (updatedRelations.length !== otherConcept.relations.length) { this.concepts.set(id, { ...otherConcept, relations: updatedRelations, }); } } } /** * Simulate memory decay for testing purposes */ async simulateDecay(milliseconds: number): Promise<void> { const decayFactor = Math.exp(-0.001 * (milliseconds / 1000)); // Small decay rate for semantic memory for (const [id, concept] of this.concepts) { // Apply decay to concept activation const updatedConcept = { ...concept, activation: concept.activation * decayFactor, }; // Remove concepts that have decayed below threshold if (updatedConcept.activation < 0.1) { this.concepts.delete(id); } else { this.concepts.set(id, updatedConcept); } } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ThoughtMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server