UNPKG

semem

Version:

Semantic Memory for Intelligent Agents

471 lines (420 loc) 15.7 kB
/** * Entity.js - RDF-based Entity implementation for Ragno * * This class represents ragno:Entity as an RDF resource following the ragno ontology * specification. Entities are named concepts extracted from text that serve as * key nodes in the knowledge graph. * * Key Features: * - First-class RDF resource following ragno:Entity * - SKOS Concept compliance for semantic interoperability * - Entry point classification for graph traversal * - Frequency tracking and provenance information * - Connection management with other entities via relationships * - Integration with existing RagnoMemoryStore patterns */ import rdf from 'rdf-ext' import RDFElement from './models/RDFElement.js' import { logger } from '../Utils.js' export default class Entity extends RDFElement { constructor(options = {}) { // Initialize with entity type super({ ...options, type: 'entity' }) // Add ragno:Entity type this.addType(this.ns.classes.Entity) // Set name/label if provided if (options.name || options.label) { const label = options.name || options.label this.setPrefLabel(label) this.setContent(label) // Also set as content for consistency } // Set entry point status (default true for entities) this.setEntryPoint(options.isEntryPoint !== undefined ? options.isEntryPoint : true) // Set sub-type if provided (e.g., "ExtractedConcept", "Person", "Organization") if (options.subType) { this.setSubType(options.subType) } // Set frequency if provided if (options.frequency !== undefined) { this.setFrequency(options.frequency) } // Set corpus association if provided if (options.corpus) { this.setCorpus(options.corpus) } logger.debug(`Created ragno:Entity: ${this.uri} with label "${this.getPrefLabel()}"`) } /** * Set the name/label for this entity * @param {string} name - Entity name * @param {string} [lang='en'] - Language tag */ setName(name, lang = 'en') { this.setPrefLabel(name, lang) this.setContent(name) // Ensure content is also set } /** * Get the SKOS prefLabel for this entity (or empty string if not set) * @returns {string} */ getPrefLabel() { const quads = [...this.dataset.match(this.node, this.ns.skosProperties.prefLabel)]; return quads.length > 0 && quads[0].object.value ? quads[0].object.value : ''; } /** * Get the name for this entity (or empty string if not set) * @returns {string} */ getName() { return this.getPrefLabel(); } /** * Set frequency for this entity (usage tracking) * @param {number} frequency - Frequency count */ setFrequency(frequency) { this.removeTriple(this.ns.ex('frequency')) this.addTriple(this.ns.ex('frequency'), rdf.literal(frequency)) } /** * Get frequency for this entity * @returns {number|null} Frequency count */ getFrequency() { const quads = this.getTriplesWithPredicate(this.ns.ex('frequency')) return quads.length > 0 ? parseInt(quads[0].object.value) : null } /** * Increment frequency counter * @param {number} [increment=1] - Amount to increment */ incrementFrequency(increment = 1) { const currentFreq = this.getFrequency() || 0 this.setFrequency(currentFreq + increment) } /** * Set corpus association for this entity * @param {string|NamedNode} corpus - Corpus URI or node */ setCorpus(corpus) { this.removeTriple(this.ns.properties.inCorpus) const corpusNode = typeof corpus === 'string' ? rdf.namedNode(corpus) : corpus this.addTriple(this.ns.properties.inCorpus, corpusNode) } /** * Get corpus association for this entity * @returns {NamedNode|null} Corpus node */ getCorpus() { const quads = this.getTriplesWithPredicate(this.ns.properties.inCorpus) return quads.length > 0 ? quads[0].object : null } /** * Set first seen timestamp * @param {Date|string} timestamp - First seen date */ setFirstSeen(timestamp) { this.removeTriple(this.ns.ex('firstSeen')) const date = timestamp instanceof Date ? timestamp : new Date(timestamp) this.addTriple( this.ns.ex('firstSeen'), rdf.literal(date.toISOString(), this.ns.xsd.dateTime) ) } /** * Get first seen timestamp * @returns {Date|null} First seen date */ getFirstSeen() { const quads = this.getTriplesWithPredicate(this.ns.ex('firstSeen')) return quads.length > 0 ? new Date(quads[0].object.value) : null } /** * Set last accessed timestamp * @param {Date|string} timestamp - Last accessed date */ setLastAccessed(timestamp) { this.removeTriple(this.ns.ex('lastAccessed')) const date = timestamp instanceof Date ? timestamp : new Date(timestamp) this.addTriple( this.ns.ex('lastAccessed'), rdf.literal(date.toISOString(), this.ns.xsd.dateTime) ) } /** * Get last accessed timestamp * @returns {Date|null} Last accessed date */ getLastAccessed() { const quads = this.getTriplesWithPredicate(this.ns.ex('lastAccessed')) return quads.length > 0 ? new Date(quads[0].object.value) : null } /** * Update last accessed timestamp to now */ touch() { this.setLastAccessed(new Date()) } /** * Add an alternative label/name for this entity * @param {string} altName - Alternative name * @param {string} [lang='en'] - Language tag */ addAlternativeName(altName, lang = 'en') { this.addAltLabel(altName, lang) } /** * Get all alternative names for this entity * @returns {Array<string>} Alternative names */ getAlternativeNames() { return this.getTriplesWithPredicate(this.ns.skosProperties.altLabel) .map(quad => quad.object.value) } /** * Add a relationship to another entity * @param {Entity|NamedNode|string} targetEntity - Target entity * @param {string} description - Relationship description * @param {number} [weight=1.0] - Relationship weight * @param {string} [relationshipType] - Type of relationship * @returns {Object} Relationship information */ addRelationshipTo(targetEntity, description, weight = 1.0, relationshipType) { // This creates a reference to a relationship but doesn't create the Relationship object // That should be done through the graph manager or relationship factory const targetNode = this._normalizeEntityReference(targetEntity) // Create a simple connection for now this.connectTo(targetNode, weight) return { source: this.node, target: targetNode, description, weight, type: relationshipType } } /** * Get all relationships involving this entity * @param {Object} [options] - Query options * @returns {Array} Relationship information */ getRelationships(options = {}) { // This would typically query the broader dataset or graph manager // For now, return direct connections const connections = this.getConnectedElements() return connections.map(target => ({ source: this.node, target, type: 'connectsTo' })) } /** * Check if this entity has a relationship with another entity * @param {Entity|NamedNode|string} otherEntity - Other entity to check * @returns {boolean} True if relationship exists */ hasRelationshipWith(otherEntity) { const otherNode = this._normalizeEntityReference(otherEntity) return this.getConnectedElements().some(node => node.equals(otherNode)) } /** * Add an attribute to this entity * @param {string} content - Attribute content * @param {string} [subType] - Attribute sub-type */ addAttribute(content, subType) { // Create attribute reference const attributeURI = `${this.uri}/attribute/${Date.now()}` const attributeNode = rdf.namedNode(attributeURI) // Link to this entity this.addTriple(this.ns.properties.hasAttribute, attributeNode) return { uri: attributeURI, content, subType, entity: this.uri } } /** * Get all attributes for this entity * @returns {Array<NamedNode>} Attribute nodes */ getAttributes() { return this.getTriplesWithPredicate(this.ns.properties.hasAttribute) .map(quad => quad.object) } /** * Add a source document to this entity as an RDF triple * @param {string} source - The source document URI or string */ addSource(source) { const sourcePredicate = this.ns.properties.hasSourceDocument || this.ns.ex('hasSourceDocument') || rdf.namedNode('http://hyperdata.it/ontologies/ragno#hasSourceDocument'); const sourceNode = typeof source === 'string' ? rdf.namedNode(source) : source; this.addTriple(sourcePredicate, sourceNode); } /** * Normalize different entity reference formats to NamedNode * @private * @param {Entity|NamedNode|string} entity - Entity reference * @returns {NamedNode} Normalized entity node */ _normalizeEntityReference(entity) { if (typeof entity === 'string') { return rdf.namedNode(entity) } else if (entity && typeof entity === 'object' && entity.node) { // Entity instance return entity.node } else if (entity && entity.termType === 'NamedNode') { // Already a NamedNode return entity } else { throw new Error(`Invalid entity reference: ${entity}`) } } /** * Validate this entity according to ragno ontology * @returns {Object} Validation result */ validate() { const baseValidation = super.validate() const errors = [...baseValidation.errors] // Check ragno:Entity type if (!this.hasType(this.ns.classes.Entity)) { errors.push('Entity must have ragno:Entity type') } // Check required label if (!this.getPrefLabel()) { errors.push('Entity must have a preferred label') } // Check entry point status is boolean const entryPoint = this.isEntryPoint() if (typeof entryPoint !== 'boolean') { errors.push('Entity entry point status must be boolean') } return { valid: errors.length === 0, errors } } /** * Get entity metadata including ragno-specific properties * @returns {Object} Entity metadata */ getMetadata() { const baseMetadata = super.getMetadata() return { ...baseMetadata, name: this.getName(), alternativeNames: this.getAlternativeNames(), frequency: this.getFrequency(), corpus: this.getCorpus()?.value, firstSeen: this.getFirstSeen(), lastAccessed: this.getLastAccessed(), relationshipCount: this.getConnectedElements().length, attributeCount: this.getAttributes().length } } /** * Convert to simple object representation (for RagnoMemoryStore compatibility) * @returns {Object} Simple object representation */ toSimpleObject() { return { uri: this.uri, label: this.getName(), name: this.getName(), frequency: this.getFrequency() || 1, isEntryPoint: this.isEntryPoint(), subType: this.getSubType(), corpus: this.getCorpus()?.value, firstSeen: this.getFirstSeen()?.toISOString(), lastAccessed: this.getLastAccessed()?.toISOString(), alternativeNames: this.getAlternativeNames() } } /** * Create entity from simple object (migration helper for RagnoMemoryStore) * @param {Object} obj - Simple object representation * @param {Object} [options] - Additional options * @returns {Entity} RDF-based entity */ static fromSimpleObject(obj, options = {}) { return new Entity({ ...options, name: obj.label || obj.name, frequency: obj.frequency, isEntryPoint: obj.isEntryPoint, subType: obj.subType, corpus: obj.corpus }) } /** * Create an entity with automatic URI generation * @param {string} name - Entity name/label * @param {Object} [options] - Additional options * @returns {Entity} Created entity */ static create(name, options = {}) { return new Entity({ ...options, name }) } /** * Generate URI for entity based on name (useful for RagnoMemoryStore integration) * @param {string} name - Entity name * @param {string} [baseURI] - Base URI * @returns {string} Generated URI */ static generateURI(name, baseURI = 'http://example.org/ragno/') { // Create a stable URI based on the name (for consistency with RagnoMemoryStore) const normalizedName = name.toLowerCase() .replace(/[^a-z0-9]/g, '_') .replace(/_+/g, '_') .replace(/^_|_$/g, '') return `${baseURI}entity/${normalizedName}` } /** * Clone this entity with optional modifications * @param {Object} [modifications] - Properties to modify in the clone * @returns {Entity} Cloned entity */ clone(modifications = {}) { const cloned = new Entity({ dataset: rdf.dataset(), // New dataset for clone name: modifications.name || this.getName(), frequency: modifications.frequency !== undefined ? modifications.frequency : this.getFrequency(), isEntryPoint: modifications.isEntryPoint !== undefined ? modifications.isEntryPoint : this.isEntryPoint(), subType: modifications.subType || this.getSubType(), corpus: modifications.corpus || this.getCorpus() }) // Copy additional properties that aren't handled by constructor for (const quad of this.getTriples()) { // Skip properties that are handled by constructor if (!quad.predicate.equals(this.ns.skosProperties.prefLabel) && !quad.predicate.equals(this.ns.properties.content) && !quad.predicate.equals(this.ns.properties.isEntryPoint) && !quad.predicate.equals(this.ns.properties.subType) && !quad.predicate.equals(this.ns.ex('frequency')) && !quad.predicate.equals(this.ns.properties.inCorpus) && !quad.predicate.equals(this.ns.dcProperties.created)) { cloned.addTriple(quad.predicate, quad.object) } } return cloned } /** * Get the preferred label (SKOS prefLabel) for this entity * @returns {string} The preferred label, or empty string if not set */ getPreferredLabel() { if (this.getPrefLabel) { const label = this.getPrefLabel(); return label ? label : ''; } return ''; } }