Class: HTM::Models::Node
- Inherits:
-
Object
- Object
- HTM::Models::Node
- Defined in:
- lib/htm/models/node.rb
Overview
Node model - represents a memory node (conversation message)
Nodes are globally unique by content (via content_hash) and can be linked to multiple robots through the robot_nodes join table.
Class Method Summary collapse
-
.find_by_content(content) ⇒ Node?
Find a node by content hash, or return nil.
-
.generate_content_hash(content) ⇒ String
Generate SHA-256 hash for content.
-
.purge_deleted(older_than:) ⇒ Integer
Permanently delete all soft-deleted nodes older than the specified time.
Instance Method Summary collapse
-
#add_tags(tag_names) ⇒ void
Add tags to this node (creates tags and all parent tags if they don’t exist).
- #before_create ⇒ Object
- #before_save ⇒ Object
-
#before_validation ⇒ Object
Hooks.
-
#deleted? ⇒ Boolean
Check if node is soft-deleted.
-
#embedding ⇒ Object
Override embedding getter to return Array instead of String pgvector stores as string format “[0.1,0.2,…]” and we need Array<Float>.
-
#embedding_array ⇒ Array<Float>?
Get embedding as an Array (handles both String and Array storage) Note: The ‘embedding` getter already returns Array, this is an alias for compatibility.
-
#nearest_neighbors(limit: 10, distance: "cosine") ⇒ Array<Node>
Find nearest neighbors to this node’s embedding.
-
#neighbor_distance ⇒ Object
Accessor for neighbor_distance from nearest_neighbors query Works with both: - Instance method (stores in @neighbor_distance) - Dataset method (stores in values hash from SELECT).
-
#proposition? ⇒ Boolean
Check if node is a proposition (extracted atomic fact).
-
#remove_tag(tag_name) ⇒ void
Remove a tag from this node.
-
#restore! ⇒ Boolean
Restore a soft-deleted node.
-
#similarity_to(other) ⇒ Float
Calculate cosine similarity to another embedding or node.
-
#soft_delete! ⇒ Boolean
Soft delete - mark node as deleted without removing from database.
-
#tag_names ⇒ Array<String>
Get all tag names associated with this node.
-
#to_hash ⇒ Hash
(also: #attributes)
Convert to hash (for compatibility with existing code).
-
#validate ⇒ Object
Validations.
Class Method Details
.find_by_content(content) ⇒ Node?
Find a node by content hash, or return nil
165 166 167 168 |
# File 'lib/htm/models/node.rb', line 165 def self.find_by_content(content) hash = generate_content_hash(content) first(content_hash: hash) end |
.generate_content_hash(content) ⇒ String
Generate SHA-256 hash for content
175 176 177 |
# File 'lib/htm/models/node.rb', line 175 def self.generate_content_hash(content) Digest::SHA256.hexdigest(content.to_s) end |
.purge_deleted(older_than:) ⇒ Integer
Permanently delete all soft-deleted nodes older than the specified time
156 157 158 |
# File 'lib/htm/models/node.rb', line 156 def self.purge_deleted(older_than:) dataset.unfiltered.where { deleted_at < older_than }.delete end |
Instance Method Details
#add_tags(tag_names) ⇒ void
This method returns an undefined value.
Add tags to this node (creates tags and all parent tags if they don’t exist)
276 277 278 279 280 281 282 |
# File 'lib/htm/models/node.rb', line 276 def (tag_names) Array(tag_names).each do |tag_name| HTM::Models::Tag.find_or_create_with_ancestors(tag_name).each do |tag| HTM::Models::NodeTag.find_or_create(node_id: id, tag_id: tag.id) end end end |
#before_create ⇒ Object
137 138 139 140 141 142 |
# File 'lib/htm/models/node.rb', line 137 def before_create self.created_at ||= Time.now self.updated_at ||= Time.now self.last_accessed ||= Time.now super end |
#before_save ⇒ Object
144 145 146 147 |
# File 'lib/htm/models/node.rb', line 144 def before_save self.updated_at = Time.now if changed_columns.any? super end |
#before_validation ⇒ Object
Hooks
130 131 132 133 134 135 |
# File 'lib/htm/models/node.rb', line 130 def before_validation if content_hash.nil? && content self.content_hash = self.class.generate_content_hash(content) end super end |
#deleted? ⇒ Boolean
Check if node is soft-deleted
339 340 341 |
# File 'lib/htm/models/node.rb', line 339 def deleted? !deleted_at.nil? end |
#embedding ⇒ Object
Override embedding getter to return Array instead of String pgvector stores as string format “[0.1,0.2,…]” and we need Array<Float>
30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/htm/models/node.rb', line 30 def raw = super return nil if raw.nil? return raw if raw.is_a?(Array) # Parse string format: "[0.1,0.2,0.3]" if raw.is_a?(String) raw.gsub(/[\[\]]/, '').split(',').map(&:to_f) else raw.to_a end end |
#embedding_array ⇒ Array<Float>?
Get embedding as an Array (handles both String and Array storage) Note: The ‘embedding` getter already returns Array, this is an alias for compatibility
231 232 233 |
# File 'lib/htm/models/node.rb', line 231 def end |
#nearest_neighbors(limit: 10, distance: "cosine") ⇒ Array<Node>
Find nearest neighbors to this node’s embedding
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/htm/models/node.rb', line 187 def nearest_neighbors(limit: 10, distance: "cosine") return [] unless # Use raw SQL for vector similarity search db = self.class.db # Handle embedding - might be String or Array depending on Sequel pg extension emb = return [] if emb.nil? || emb.empty? vector_str = "[#{emb.join(',')}]" sql = <<-SQL SELECT nodes.*, (embedding <=> '#{vector_str}'::vector) AS neighbor_distance FROM nodes WHERE embedding IS NOT NULL AND deleted_at IS NULL AND id != #{id} ORDER BY embedding <=> '#{vector_str}'::vector LIMIT #{limit} SQL # Use call() to create instances from raw hashes without mass assignment restrictions db.fetch(sql).all.map do |row| node = self.class.call(row) # Store neighbor_distance as an instance variable node.instance_variable_set(:@neighbor_distance, row[:neighbor_distance]) node end end |
#neighbor_distance ⇒ Object
Accessor for neighbor_distance from nearest_neighbors query Works with both:
-
Instance method (stores in @neighbor_distance)
-
Dataset method (stores in values hash from SELECT)
222 223 224 |
# File 'lib/htm/models/node.rb', line 222 def neighbor_distance @neighbor_distance || values[:neighbor_distance] end |
#proposition? ⇒ Boolean
Check if node is a proposition (extracted atomic fact)
347 348 349 |
# File 'lib/htm/models/node.rb', line 347 def proposition? &.dig('is_proposition') == true end |
#remove_tag(tag_name) ⇒ void
This method returns an undefined value.
Remove a tag from this node
289 290 291 292 293 294 |
# File 'lib/htm/models/node.rb', line 289 def remove_tag(tag_name) tag = HTM::Models::Tag.first(name: tag_name) return unless tag .where(tag_id: tag.id).delete end |
#restore! ⇒ Boolean
Restore a soft-deleted node
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 |
# File 'lib/htm/models/node.rb', line 318 def restore! db.transaction do # Use unfiltered dataset to bypass the default scope that excludes deleted records self.class.dataset.unfiltered.where(id: id).update(deleted_at: nil) # Cascade restoration to associated robot_nodes HTM::Models::RobotNode.dataset.unfiltered.where(node_id: id).update(deleted_at: nil) # Cascade restoration to associated node_tags HTM::Models::NodeTag.dataset.unfiltered.where(node_id: id).update(deleted_at: nil) # Refresh this instance to reflect the change self.deleted_at = nil end true end |
#similarity_to(other) ⇒ Float
Calculate cosine similarity to another embedding or node
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/htm/models/node.rb', line 240 def similarity_to(other) = other.is_a?(Node) ? other. : other return nil unless && # Handle query_embedding that might be a String if .is_a?(String) = .gsub(/[\[\]]/, '').split(',').map(&:to_f) end unless .is_a?(Array) && .all? { |v| v.is_a?(Numeric) && v.finite? } return nil end vector_str = "[#{.map(&:to_f).join(',')}]" result = self.class.db.fetch( "SELECT 1 - (embedding <=> ?::vector) AS similarity FROM nodes WHERE id = ?", vector_str, id ).first result&.[](:similarity)&.to_f end |
#soft_delete! ⇒ Boolean
Soft delete - mark node as deleted without removing from database
300 301 302 303 304 305 306 307 308 309 310 311 312 |
# File 'lib/htm/models/node.rb', line 300 def soft_delete! db.transaction do now = Time.now update(deleted_at: now) # Cascade soft delete to associated robot_nodes HTM::Models::RobotNode.where(node_id: id).update(deleted_at: now) # Cascade soft delete to associated node_tags HTM::Models::NodeTag.where(node_id: id).update(deleted_at: now) end true end |
#tag_names ⇒ Array<String>
Get all tag names associated with this node
267 268 269 |
# File 'lib/htm/models/node.rb', line 267 def tag_names .select_map(:name) end |
#to_hash ⇒ Hash Also known as: attributes
Convert to hash (for compatibility with existing code)
355 356 357 |
# File 'lib/htm/models/node.rb', line 355 def to_hash values.transform_keys(&:to_s) end |
#validate ⇒ Object
Validations
44 45 46 47 48 |
# File 'lib/htm/models/node.rb', line 44 def validate super validates_presence %i[content content_hash] validates_unique :content_hash end |