UNPKG

semem

Version:

Semantic Memory for Intelligent Agents

515 lines (461 loc) 17.7 kB
/** * Attribute.js - RDF-based Attribute implementation for Ragno * * This class represents ragno:Attribute as an RDF resource following the ragno ontology * specification. Attributes describe properties or characteristics of entities, * providing detailed information that enhances entity understanding. * * Key Features: * - First-class RDF resource following ragno:Attribute * - SKOS Concept compliance for semantic interoperability * - Entity association via ragno:hasAttribute relationship * - Sub-type classification (Overview, Technical, Historical, etc.) * - Provenance tracking for source information * - Summary and detailed content management */ import rdf from 'rdf-ext' import RDFElement from './models/RDFElement.js' import { logger } from '../Utils.js' export default class Attribute extends RDFElement { constructor(options = {}) { // Initialize with attribute type super({ ...options, type: 'attribute' }) // Add ragno:Attribute type this.addType(this.ns.classes.Attribute) // Set text content if provided if (options.text || options.content) { const content = options.text || options.content this.setContent(content) } // Set summary if provided (as SKOS definition) if (options.summary) { this.setSummary(options.summary) } // Set entity association if provided if (options.entity) { this.setEntity(options.entity) } // Set sub-type if provided (e.g., "Overview", "Technical", "Historical") if (options.subType) { this.setSubType(options.subType) } // Set provenance information if provided if (options.provenance) { this.setProvenance(options.provenance) } // Attributes are typically not entry points (entities are) this.setEntryPoint(options.isEntryPoint !== undefined ? options.isEntryPoint : false) // Set confidence score if provided if (options.confidence !== undefined) { this.setConfidence(options.confidence) } logger.debug(`Created ragno:Attribute: ${this.uri}`) } /** * Set the main text content for this attribute * @param {string} text - Text content */ setText(text) { this.setContent(text) } /** * Get the main text content of this attribute * @returns {string|null} Text content */ getText() { return this.getContent() } /** * Set summary for this attribute (stored as SKOS definition) * @param {string} summary - Summary text * @param {string} [lang='en'] - Language tag */ setSummary(summary, lang = 'en') { this.removeTriple(this.ns.skosProperties.definition) this.addTriple(this.ns.skosProperties.definition, rdf.literal(summary, lang)) } /** * Get summary for this attribute * @returns {string|null} Summary text */ getSummary() { const quads = this.getTriplesWithPredicate(this.ns.skosProperties.definition) return quads.length > 0 ? quads[0].object.value : null } /** * Set the entity this attribute describes * @param {Entity|NamedNode|string} entity - Entity reference */ setEntity(entity) { this.removeTriple(this.ns.ex('describesEntity')) const entityNode = this._normalizeEntityReference(entity) this.addTriple(this.ns.ex('describesEntity'), entityNode) } /** * Get the entity this attribute describes * @returns {NamedNode|null} Entity node */ getEntity() { const quads = this.getTriplesWithPredicate(this.ns.ex('describesEntity')) return quads.length > 0 ? quads[0].object : null } /** * Set provenance information for this attribute * @param {string|NamedNode} provenance - Provenance source URI or node */ setProvenance(provenance) { const provenanceNode = typeof provenance === 'string' ? rdf.namedNode(provenance) : provenance this.addProvenance(provenanceNode.value) } /** * Get provenance information for this attribute * @returns {Array<NamedNode>} Provenance nodes */ getProvenance() { return this.getTriplesWithPredicate(this.ns.provProperties.wasDerivedFrom) .map(quad => quad.object) } /** * Set confidence score for this attribute * @param {number} confidence - Confidence score (0-1) */ setConfidence(confidence) { this.removeTriple(this.ns.ex('confidence')) this.addTriple(this.ns.ex('confidence'), rdf.literal(confidence)) } /** * Get confidence score for this attribute * @returns {number|null} Confidence score */ getConfidence() { const quads = this.getTriplesWithPredicate(this.ns.ex('confidence')) return quads.length > 0 ? parseFloat(quads[0].object.value) : null } /** * Set the attribute category/type (more specific than subType) * @param {string} category - Attribute category */ setCategory(category) { this.removeTriple(this.ns.ex('category')) this.addTriple(this.ns.ex('category'), rdf.literal(category)) } /** * Get the attribute category/type * @returns {string|null} Attribute category */ getCategory() { const quads = this.getTriplesWithPredicate(this.ns.ex('category')) return quads.length > 0 ? quads[0].object.value : null } /** * Set temporal information for this attribute (when it was true/relevant) * @param {Date|string} timestamp - Temporal information */ setTemporal(timestamp) { this.removeTriple(this.ns.ex('temporal')) const date = timestamp instanceof Date ? timestamp : new Date(timestamp) this.addTriple( this.ns.ex('temporal'), rdf.literal(date.toISOString(), this.ns.xsd.dateTime) ) } /** * Get temporal information for this attribute * @returns {Date|null} Temporal information */ getTemporal() { const quads = this.getTriplesWithPredicate(this.ns.ex('temporal')) return quads.length > 0 ? new Date(quads[0].object.value) : null } /** * Set corpus association for this attribute * @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 attribute * @returns {NamedNode|null} Corpus node */ getCorpus() { const quads = this.getTriplesWithPredicate(this.ns.properties.inCorpus) return quads.length > 0 ? quads[0].object : null } /** * Add supporting evidence for this attribute * @param {SemanticUnit|NamedNode|string} evidence - Evidence source (e.g., semantic unit) */ addEvidence(evidence) { const evidenceNode = this._normalizeUnitReference(evidence) this.addTriple(this.ns.provProperties.used, evidenceNode) } /** * Get all evidence sources for this attribute * @returns {Array<NamedNode>} Evidence nodes */ getEvidence() { return this.getTriplesWithPredicate(this.ns.provProperties.used) .map(quad => quad.object) } /** * Set language for this attribute * @param {string} language - Language code (e.g., 'en', 'es') */ setLanguage(language) { this.removeTriple(this.ns.dcProperties.language) this.addTriple(this.ns.dcProperties.language, rdf.literal(language)) } /** * Get language for this attribute * @returns {string|null} Language code */ getLanguage() { const quads = this.getTriplesWithPredicate(this.ns.dcProperties.language) return quads.length > 0 ? quads[0].object.value : null } /** * Add a keyword/tag to this attribute * @param {string} keyword - Keyword or tag */ addKeyword(keyword) { this.addTriple(this.ns.ex('keyword'), rdf.literal(keyword)) } /** * Get all keywords/tags for this attribute * @returns {Array<string>} Keywords */ getKeywords() { return this.getTriplesWithPredicate(this.ns.ex('keyword')) .map(quad => quad.object.value) } /** * Check if this attribute is relevant to a specific time period * @param {Date} date - Date to check * @returns {boolean} True if attribute is relevant at the given date */ isRelevantAt(date) { const temporal = this.getTemporal() if (!temporal) { return true // No temporal constraint means always relevant } // Simple check - could be enhanced with time ranges return temporal <= date } /** * 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}`) } } /** * Normalize different unit reference formats to NamedNode * @private * @param {SemanticUnit|NamedNode|string} unit - Unit reference * @returns {NamedNode} Normalized unit node */ _normalizeUnitReference(unit) { if (typeof unit === 'string') { return rdf.namedNode(unit) } else if (unit && typeof unit === 'object' && unit.node) { // SemanticUnit instance return unit.node } else if (unit && unit.termType === 'NamedNode') { // Already a NamedNode return unit } else { throw new Error(`Invalid unit reference: ${unit}`) } } /** * Validate this attribute according to ragno ontology * @returns {Object} Validation result */ validate() { const baseValidation = super.validate() const errors = [...baseValidation.errors] // Check ragno:Attribute type if (!this.hasType(this.ns.classes.Attribute)) { errors.push('Attribute must have ragno:Attribute type') } // Check required content if (!this.getText()) { errors.push('Attribute must have text content') } // Check entity association (attributes should describe an entity) if (!this.getEntity()) { errors.push('Attribute should be associated with an entity') } // Check confidence score is valid range const confidence = this.getConfidence() if (confidence !== null && (confidence < 0 || confidence > 1)) { errors.push('Attribute confidence score must be between 0 and 1') } return { valid: errors.length === 0, errors } } /** * Get attribute metadata including ragno-specific properties * @returns {Object} Attribute metadata */ getMetadata() { const baseMetadata = super.getMetadata() return { ...baseMetadata, text: this.getText(), summary: this.getSummary(), entity: this.getEntity()?.value, category: this.getCategory(), confidence: this.getConfidence(), temporal: this.getTemporal(), language: this.getLanguage(), corpus: this.getCorpus()?.value, provenanceCount: this.getProvenance().length, evidenceCount: this.getEvidence().length, keywordCount: this.getKeywords().length } } /** * Convert to simple object representation (for backward compatibility) * @returns {Object} Simple object representation */ toSimpleObject() { return { uri: this.uri, text: this.getText(), summary: this.getSummary(), entity: this.getEntity()?.value, category: this.getCategory(), subType: this.getSubType(), confidence: this.getConfidence(), temporal: this.getTemporal()?.toISOString(), language: this.getLanguage(), corpus: this.getCorpus()?.value, provenance: this.getProvenance().map(p => p.value), keywords: this.getKeywords(), isEntryPoint: this.isEntryPoint() } } /** * Create attribute from simple object (migration helper) * @param {Object} obj - Simple object representation * @param {Object} [options] - Additional options * @returns {Attribute} RDF-based attribute */ static fromSimpleObject(obj, options = {}) { const attribute = new Attribute({ ...options, text: obj.text, summary: obj.summary, entity: obj.entity, category: obj.category, subType: obj.subType, confidence: obj.confidence, temporal: obj.temporal, language: obj.language, corpus: obj.corpus, isEntryPoint: obj.isEntryPoint }) // Add provenance if provided if (obj.provenance) { if (Array.isArray(obj.provenance)) { obj.provenance.forEach(p => attribute.setProvenance(p)) } else { attribute.setProvenance(obj.provenance) } } // Add keywords if provided if (obj.keywords && Array.isArray(obj.keywords)) { obj.keywords.forEach(keyword => attribute.addKeyword(keyword)) } return attribute } /** * Create an attribute with automatic URI generation * @param {string} text - Attribute text content * @param {Entity|NamedNode|string} entity - Associated entity * @param {Object} [options] - Additional options * @returns {Attribute} Created attribute */ static create(text, entity, options = {}) { return new Attribute({ ...options, text, entity }) } /** * Create an overview attribute for an entity (common pattern) * @param {Entity|NamedNode|string} entity - Associated entity * @param {string} text - Overview text * @param {Object} [options] - Additional options * @returns {Attribute} Created overview attribute */ static createOverview(entity, text, options = {}) { return new Attribute({ ...options, text, entity, subType: 'Overview', category: 'Description' }) } /** * Clone this attribute with optional modifications * @param {Object} [modifications] - Properties to modify in the clone * @returns {Attribute} Cloned attribute */ clone(modifications = {}) { const cloned = new Attribute({ dataset: rdf.dataset(), // New dataset for clone text: modifications.text || this.getText(), summary: modifications.summary || this.getSummary(), entity: modifications.entity || this.getEntity(), category: modifications.category || this.getCategory(), subType: modifications.subType || this.getSubType(), confidence: modifications.confidence !== undefined ? modifications.confidence : this.getConfidence(), temporal: modifications.temporal || this.getTemporal(), language: modifications.language || this.getLanguage(), corpus: modifications.corpus || this.getCorpus(), isEntryPoint: modifications.isEntryPoint !== undefined ? modifications.isEntryPoint : this.isEntryPoint() }) // 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.properties.content) && !quad.predicate.equals(this.ns.skosProperties.definition) && !quad.predicate.equals(this.ns.ex('describesEntity')) && !quad.predicate.equals(this.ns.ex('category')) && !quad.predicate.equals(this.ns.properties.subType) && !quad.predicate.equals(this.ns.ex('confidence')) && !quad.predicate.equals(this.ns.ex('temporal')) && !quad.predicate.equals(this.ns.dcProperties.language) && !quad.predicate.equals(this.ns.properties.inCorpus) && !quad.predicate.equals(this.ns.properties.isEntryPoint) && !quad.predicate.equals(this.ns.dcProperties.created)) { cloned.addTriple(quad.predicate, quad.object) } } return cloned } }