htm 0.0.11 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.dictate.toml +46 -0
- data/.envrc +2 -0
- data/CHANGELOG.md +52 -2
- data/README.md +79 -0
- data/Rakefile +14 -2
- data/bin/htm_mcp.rb +94 -0
- data/config/database.yml +20 -13
- data/db/migrate/00010_add_soft_delete_to_associations.rb +29 -0
- data/db/migrate/00011_add_performance_indexes.rb +21 -0
- data/db/migrate/00012_add_tags_trigram_index.rb +18 -0
- data/db/migrate/00013_enable_lz4_compression.rb +43 -0
- data/db/schema.sql +49 -92
- data/docs/api/index.md +1 -1
- data/docs/api/yard/HTM.md +2 -4
- data/docs/architecture/index.md +1 -1
- data/docs/development/index.md +1 -1
- data/docs/getting-started/index.md +1 -1
- data/docs/guides/index.md +1 -1
- data/docs/images/telemetry-architecture.svg +153 -0
- data/docs/telemetry.md +391 -0
- data/examples/README.md +46 -1
- data/examples/cli_app/README.md +1 -1
- data/examples/cli_app/htm_cli.rb +1 -1
- data/examples/sinatra_app/app.rb +1 -1
- data/examples/telemetry/README.md +147 -0
- data/examples/telemetry/SETUP_README.md +169 -0
- data/examples/telemetry/demo.rb +498 -0
- data/examples/telemetry/grafana/dashboards/htm-metrics.json +457 -0
- data/lib/htm/configuration.rb +261 -70
- data/lib/htm/database.rb +46 -22
- data/lib/htm/embedding_service.rb +24 -14
- data/lib/htm/errors.rb +15 -1
- data/lib/htm/jobs/generate_embedding_job.rb +19 -0
- data/lib/htm/jobs/generate_propositions_job.rb +103 -0
- data/lib/htm/jobs/generate_tags_job.rb +24 -0
- data/lib/htm/loaders/markdown_chunker.rb +79 -0
- data/lib/htm/loaders/markdown_loader.rb +41 -15
- data/lib/htm/long_term_memory/fulltext_search.rb +138 -0
- data/lib/htm/long_term_memory/hybrid_search.rb +324 -0
- data/lib/htm/long_term_memory/node_operations.rb +209 -0
- data/lib/htm/long_term_memory/relevance_scorer.rb +355 -0
- data/lib/htm/long_term_memory/robot_operations.rb +34 -0
- data/lib/htm/long_term_memory/tag_operations.rb +428 -0
- data/lib/htm/long_term_memory/vector_search.rb +109 -0
- data/lib/htm/long_term_memory.rb +51 -1153
- data/lib/htm/models/node.rb +35 -2
- data/lib/htm/models/node_tag.rb +31 -0
- data/lib/htm/models/robot_node.rb +31 -0
- data/lib/htm/models/tag.rb +44 -0
- data/lib/htm/proposition_service.rb +169 -0
- data/lib/htm/query_cache.rb +214 -0
- data/lib/htm/sql_builder.rb +178 -0
- data/lib/htm/tag_service.rb +16 -6
- data/lib/htm/tasks.rb +8 -2
- data/lib/htm/telemetry.rb +224 -0
- data/lib/htm/version.rb +1 -1
- data/lib/htm.rb +64 -3
- data/lib/tasks/doc.rake +1 -1
- data/lib/tasks/htm.rake +259 -13
- data/mkdocs.yml +96 -96
- metadata +42 -16
- data/.aigcm_msg +0 -1
- data/.claude/settings.local.json +0 -95
- data/CLAUDE.md +0 -603
- data/examples/cli_app/temp.log +0 -93
- data/lib/htm/loaders/paragraph_chunker.rb +0 -112
- data/notes/ARCHITECTURE_REVIEW.md +0 -1167
- data/notes/IMPLEMENTATION_SUMMARY.md +0 -606
- data/notes/MULTI_FRAMEWORK_IMPLEMENTATION.md +0 -451
- data/notes/next_steps.md +0 -100
- data/notes/plan.md +0 -627
- data/notes/tag_ontology_enhancement_ideas.md +0 -222
- data/notes/timescaledb_removal_summary.md +0 -200
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class HTM
|
|
4
|
+
class LongTermMemory
|
|
5
|
+
# Relevance scoring for search results
|
|
6
|
+
#
|
|
7
|
+
# Combines multiple signals to calculate dynamic relevance:
|
|
8
|
+
# - Vector similarity (semantic match) - config.relevance_semantic_weight (default: 0.5)
|
|
9
|
+
# - Tag overlap (categorical match) - config.relevance_tag_weight (default: 0.3)
|
|
10
|
+
# - Recency (freshness) - config.relevance_recency_weight (default: 0.1)
|
|
11
|
+
# - Access frequency (popularity/utility) - config.relevance_access_weight (default: 0.1)
|
|
12
|
+
#
|
|
13
|
+
# Recency decay uses configurable half-life: config.relevance_recency_half_life_hours (default: 168 = 1 week)
|
|
14
|
+
#
|
|
15
|
+
# Also provides tag similarity calculations using hierarchical Jaccard.
|
|
16
|
+
#
|
|
17
|
+
module RelevanceScorer
|
|
18
|
+
# Default score when signal is unavailable
|
|
19
|
+
DEFAULT_NEUTRAL_SCORE = 0.5
|
|
20
|
+
|
|
21
|
+
# Access frequency normalization
|
|
22
|
+
ACCESS_SCORE_NORMALIZER = 10.0
|
|
23
|
+
|
|
24
|
+
# Final score scaling
|
|
25
|
+
RELEVANCE_SCALE = 10.0
|
|
26
|
+
RELEVANCE_MIN = 0.0
|
|
27
|
+
RELEVANCE_MAX = 10.0
|
|
28
|
+
|
|
29
|
+
# Configurable scoring weights (via HTM.configuration)
|
|
30
|
+
def weight_semantic
|
|
31
|
+
HTM.configuration.relevance_semantic_weight
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def weight_tag
|
|
35
|
+
HTM.configuration.relevance_tag_weight
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def weight_recency
|
|
39
|
+
HTM.configuration.relevance_recency_weight
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def weight_access
|
|
43
|
+
HTM.configuration.relevance_access_weight
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def recency_half_life_hours
|
|
47
|
+
HTM.configuration.relevance_recency_half_life_hours
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Calculate dynamic relevance score for a node given query context
|
|
51
|
+
#
|
|
52
|
+
# @param node [Hash] Node data with similarity, tags, created_at, access_count
|
|
53
|
+
# @param query_tags [Array<String>] Tags associated with the query
|
|
54
|
+
# @param vector_similarity [Float, nil] Pre-computed vector similarity (0-1)
|
|
55
|
+
# @param node_tags [Array<String>, nil] Pre-loaded tags for this node (avoids N+1 query)
|
|
56
|
+
# @return [Float] Composite relevance score (RELEVANCE_MIN to RELEVANCE_MAX)
|
|
57
|
+
#
|
|
58
|
+
def calculate_relevance(node:, query_tags: [], vector_similarity: nil, node_tags: nil)
|
|
59
|
+
# 1. Vector similarity (semantic match)
|
|
60
|
+
semantic_score = if vector_similarity
|
|
61
|
+
vector_similarity
|
|
62
|
+
elsif node['similarity']
|
|
63
|
+
node['similarity'].to_f
|
|
64
|
+
else
|
|
65
|
+
DEFAULT_NEUTRAL_SCORE # Neutral if no embedding
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# 2. Tag overlap (categorical relevance)
|
|
69
|
+
# Use pre-loaded tags if provided, otherwise fetch (for backward compatibility)
|
|
70
|
+
node_tags ||= get_node_tags(node['id'])
|
|
71
|
+
tag_score = if query_tags.any? && node_tags.any?
|
|
72
|
+
weighted_hierarchical_jaccard(query_tags, node_tags)
|
|
73
|
+
else
|
|
74
|
+
DEFAULT_NEUTRAL_SCORE # Neutral if no tags
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# 3. Recency (temporal relevance) - exponential decay with half-life
|
|
78
|
+
age_hours = (Time.now - Time.parse(node['created_at'].to_s)) / 3600.0
|
|
79
|
+
recency_score = Math.exp(-age_hours / recency_half_life_hours)
|
|
80
|
+
|
|
81
|
+
# 4. Access frequency (behavioral signal) - log-normalized
|
|
82
|
+
access_count = node['access_count'] || 0
|
|
83
|
+
access_score = Math.log(1 + access_count) / ACCESS_SCORE_NORMALIZER
|
|
84
|
+
|
|
85
|
+
# Weighted composite with final scaling
|
|
86
|
+
relevance = (
|
|
87
|
+
(semantic_score * weight_semantic) +
|
|
88
|
+
(tag_score * weight_tag) +
|
|
89
|
+
(recency_score * weight_recency) +
|
|
90
|
+
(access_score * weight_access)
|
|
91
|
+
) * RELEVANCE_SCALE
|
|
92
|
+
|
|
93
|
+
relevance.clamp(RELEVANCE_MIN, RELEVANCE_MAX)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Search with dynamic relevance scoring
|
|
97
|
+
#
|
|
98
|
+
# Returns nodes with calculated relevance scores based on query context
|
|
99
|
+
#
|
|
100
|
+
# @param timeframe [nil, Range, Array<Range>] Time range(s) to search (nil = no filter)
|
|
101
|
+
# @param query [String, nil] Search query
|
|
102
|
+
# @param query_tags [Array<String>] Tags to match
|
|
103
|
+
# @param limit [Integer] Maximum results
|
|
104
|
+
# @param embedding_service [Object, nil] Service to generate embeddings
|
|
105
|
+
# @param metadata [Hash] Filter by metadata fields (default: {})
|
|
106
|
+
# @return [Array<Hash>] Nodes with relevance scores
|
|
107
|
+
#
|
|
108
|
+
def search_with_relevance(timeframe:, query: nil, query_tags: [], limit: 20, embedding_service: nil, metadata: {})
|
|
109
|
+
# Get candidates from appropriate search method
|
|
110
|
+
candidates = if query && embedding_service
|
|
111
|
+
# Vector search (returns hashes directly)
|
|
112
|
+
search_uncached(timeframe: timeframe, query: query, limit: limit * 2, embedding_service: embedding_service, metadata: metadata)
|
|
113
|
+
elsif query
|
|
114
|
+
# Full-text search (returns hashes directly)
|
|
115
|
+
search_fulltext_uncached(timeframe: timeframe, query: query, limit: limit * 2, metadata: metadata)
|
|
116
|
+
else
|
|
117
|
+
# Time-range only - use raw SQL to avoid ActiveRecord object instantiation
|
|
118
|
+
# This is more efficient than .map(&:attributes) which creates intermediate objects
|
|
119
|
+
fetch_candidates_by_timeframe(timeframe: timeframe, metadata: metadata, limit: limit * 2)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Batch load all tags for candidates (fixes N+1 query)
|
|
123
|
+
node_ids = candidates.map { |n| n['id'] }
|
|
124
|
+
tags_by_node = batch_load_node_tags(node_ids)
|
|
125
|
+
|
|
126
|
+
# Calculate relevance for each candidate, building final hash in-place
|
|
127
|
+
scored_nodes = candidates.map do |node|
|
|
128
|
+
node_tags = tags_by_node[node['id']] || []
|
|
129
|
+
|
|
130
|
+
relevance = calculate_relevance(
|
|
131
|
+
node: node,
|
|
132
|
+
query_tags: query_tags,
|
|
133
|
+
vector_similarity: node['similarity']&.to_f,
|
|
134
|
+
node_tags: node_tags
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Modify in-place to avoid creating new Hash
|
|
138
|
+
node['relevance'] = relevance
|
|
139
|
+
node['tags'] = node_tags
|
|
140
|
+
node
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Sort by relevance and return top K
|
|
144
|
+
scored_nodes
|
|
145
|
+
.sort_by { |n| -n['relevance'] }
|
|
146
|
+
.take(limit)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Fetch candidates by timeframe using raw SQL (avoids ActiveRecord overhead)
|
|
150
|
+
#
|
|
151
|
+
# @param timeframe [nil, Range, Array<Range>] Time range(s) to search
|
|
152
|
+
# @param metadata [Hash] Filter by metadata fields
|
|
153
|
+
# @param limit [Integer] Maximum results
|
|
154
|
+
# @return [Array<Hash>] Candidate nodes as hashes
|
|
155
|
+
#
|
|
156
|
+
def fetch_candidates_by_timeframe(timeframe:, metadata:, limit:)
|
|
157
|
+
timeframe_condition = HTM::SqlBuilder.timeframe_condition(timeframe)
|
|
158
|
+
metadata_condition = HTM::SqlBuilder.metadata_condition(metadata)
|
|
159
|
+
|
|
160
|
+
conditions = ['deleted_at IS NULL']
|
|
161
|
+
conditions << timeframe_condition if timeframe_condition
|
|
162
|
+
conditions << metadata_condition if metadata_condition
|
|
163
|
+
|
|
164
|
+
sql = <<~SQL
|
|
165
|
+
SELECT id, content, access_count, created_at, token_count
|
|
166
|
+
FROM nodes
|
|
167
|
+
WHERE #{conditions.join(' AND ')}
|
|
168
|
+
ORDER BY created_at DESC
|
|
169
|
+
LIMIT ?
|
|
170
|
+
SQL
|
|
171
|
+
|
|
172
|
+
result = ActiveRecord::Base.connection.select_all(
|
|
173
|
+
ActiveRecord::Base.sanitize_sql_array([sql, limit])
|
|
174
|
+
)
|
|
175
|
+
result.to_a
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Search nodes by tags
|
|
179
|
+
#
|
|
180
|
+
# @param tags [Array<String>] Tags to search for
|
|
181
|
+
# @param match_all [Boolean] If true, match ALL tags; if false, match ANY tag
|
|
182
|
+
# @param timeframe [Range, nil] Optional time range filter
|
|
183
|
+
# @param limit [Integer] Maximum results
|
|
184
|
+
# @return [Array<Hash>] Matching nodes with relevance scores
|
|
185
|
+
#
|
|
186
|
+
def search_by_tags(tags:, match_all: false, timeframe: nil, limit: 20)
|
|
187
|
+
return [] if tags.empty?
|
|
188
|
+
|
|
189
|
+
# Build base query with specific columns to avoid loading unnecessary data
|
|
190
|
+
query = HTM::Models::Node
|
|
191
|
+
.select('nodes.id, nodes.content, nodes.access_count, nodes.created_at, nodes.token_count')
|
|
192
|
+
.joins(:tags)
|
|
193
|
+
.where(tags: { name: tags })
|
|
194
|
+
.distinct
|
|
195
|
+
|
|
196
|
+
# Apply timeframe filter if provided
|
|
197
|
+
query = query.where(created_at: timeframe) if timeframe
|
|
198
|
+
|
|
199
|
+
if match_all
|
|
200
|
+
# Match ALL tags (intersection)
|
|
201
|
+
query = query
|
|
202
|
+
.group('nodes.id')
|
|
203
|
+
.having('COUNT(DISTINCT tags.name) = ?', tags.size)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Convert to hashes efficiently using pluck-style approach
|
|
207
|
+
# This avoids instantiating full ActiveRecord objects
|
|
208
|
+
nodes = query.limit(limit).map do |node|
|
|
209
|
+
{
|
|
210
|
+
'id' => node.id,
|
|
211
|
+
'content' => node.content,
|
|
212
|
+
'access_count' => node.access_count,
|
|
213
|
+
'created_at' => node.created_at,
|
|
214
|
+
'token_count' => node.token_count
|
|
215
|
+
}
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Batch load all tags for nodes (fixes N+1 query)
|
|
219
|
+
node_ids = nodes.map { |n| n['id'] }
|
|
220
|
+
tags_by_node = batch_load_node_tags(node_ids)
|
|
221
|
+
|
|
222
|
+
# Calculate relevance and enrich with tags (modify in-place)
|
|
223
|
+
nodes.map do |node|
|
|
224
|
+
node_tags = tags_by_node[node['id']] || []
|
|
225
|
+
relevance = calculate_relevance(
|
|
226
|
+
node: node,
|
|
227
|
+
query_tags: tags,
|
|
228
|
+
node_tags: node_tags
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
node['relevance'] = relevance
|
|
232
|
+
node['tags'] = node_tags
|
|
233
|
+
node
|
|
234
|
+
end.sort_by { |n| -n['relevance'] }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
# Calculate Jaccard similarity between two sets
|
|
240
|
+
#
|
|
241
|
+
# @param set_a [Array] First set
|
|
242
|
+
# @param set_b [Array] Second set
|
|
243
|
+
# @return [Float] Jaccard similarity (0.0-1.0)
|
|
244
|
+
#
|
|
245
|
+
def jaccard_similarity(set_a, set_b)
|
|
246
|
+
return 0.0 if set_a.empty? && set_b.empty?
|
|
247
|
+
return 0.0 if set_a.empty? || set_b.empty?
|
|
248
|
+
|
|
249
|
+
intersection = (set_a & set_b).size
|
|
250
|
+
union = (set_a | set_b).size
|
|
251
|
+
|
|
252
|
+
intersection.to_f / union
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Calculate weighted hierarchical Jaccard similarity
|
|
256
|
+
#
|
|
257
|
+
# Compares hierarchical tags accounting for partial matches at different levels.
|
|
258
|
+
# Optimized to pre-compute tag hierarchies and use early termination.
|
|
259
|
+
#
|
|
260
|
+
# Performance: O(n*m) where n,m are tag counts, but with:
|
|
261
|
+
# - Pre-computed splits to avoid repeated String#split
|
|
262
|
+
# - Early termination when root categories don't match
|
|
263
|
+
# - Set-based exact match fast path
|
|
264
|
+
#
|
|
265
|
+
# @param set_a [Array<String>] First set of hierarchical tags
|
|
266
|
+
# @param set_b [Array<String>] Second set of hierarchical tags
|
|
267
|
+
# @return [Float] Weighted similarity (0.0-1.0)
|
|
268
|
+
#
|
|
269
|
+
def weighted_hierarchical_jaccard(set_a, set_b)
|
|
270
|
+
return 0.0 if set_a.empty? || set_b.empty?
|
|
271
|
+
|
|
272
|
+
# Fast path: check for exact matches first
|
|
273
|
+
exact_matches = (set_a & set_b).size
|
|
274
|
+
return 1.0 if exact_matches == set_a.size && exact_matches == set_b.size
|
|
275
|
+
|
|
276
|
+
# Pre-compute tag hierarchies to avoid repeated String#split
|
|
277
|
+
hierarchies_a = set_a.map { |tag| tag.split(':') }
|
|
278
|
+
hierarchies_b = set_b.map { |tag| tag.split(':') }
|
|
279
|
+
|
|
280
|
+
# Build root category index for early termination optimization
|
|
281
|
+
# Group tags by their root category for faster matching
|
|
282
|
+
roots_b = hierarchies_b.group_by(&:first)
|
|
283
|
+
|
|
284
|
+
total_weighted_similarity = 0.0
|
|
285
|
+
total_weights = 0.0
|
|
286
|
+
|
|
287
|
+
hierarchies_a.each do |parts_a|
|
|
288
|
+
root_a = parts_a.first
|
|
289
|
+
|
|
290
|
+
# Only compare with tags that share the same root category
|
|
291
|
+
matching_hierarchies = roots_b[root_a] || []
|
|
292
|
+
|
|
293
|
+
# Also include all hierarchies if no root match (for cross-category comparison)
|
|
294
|
+
candidates = matching_hierarchies.empty? ? hierarchies_b : matching_hierarchies
|
|
295
|
+
|
|
296
|
+
candidates.each do |parts_b|
|
|
297
|
+
similarity, weight = calculate_hierarchical_similarity_cached(parts_a, parts_b)
|
|
298
|
+
total_weighted_similarity += similarity * weight
|
|
299
|
+
total_weights += weight
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Add zero-similarity weight for non-matching root categories
|
|
303
|
+
(hierarchies_b.size - candidates.size).times do
|
|
304
|
+
# Non-matching roots contribute weight but zero similarity
|
|
305
|
+
total_weights += 0.5 # Average weight for non-matches
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
total_weights > 0 ? total_weighted_similarity / total_weights : 0.0
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Calculate similarity between two pre-split hierarchical tags
|
|
313
|
+
#
|
|
314
|
+
# Optimized version that takes pre-split arrays to avoid redundant splits.
|
|
315
|
+
#
|
|
316
|
+
# @param parts_a [Array<String>] First tag hierarchy (pre-split)
|
|
317
|
+
# @param parts_b [Array<String>] Second tag hierarchy (pre-split)
|
|
318
|
+
# @return [Array<Float, Float>] [similarity, weight] both in range 0.0-1.0
|
|
319
|
+
#
|
|
320
|
+
def calculate_hierarchical_similarity_cached(parts_a, parts_b)
|
|
321
|
+
# Calculate overlap at each level using zip for efficiency
|
|
322
|
+
max_depth = [parts_a.length, parts_b.length].max
|
|
323
|
+
min_depth = [parts_a.length, parts_b.length].min
|
|
324
|
+
|
|
325
|
+
# Count common levels from root
|
|
326
|
+
common_levels = 0
|
|
327
|
+
min_depth.times do |i|
|
|
328
|
+
break unless parts_a[i] == parts_b[i]
|
|
329
|
+
common_levels += 1
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Weight based on hierarchy depth (deeper = less weight)
|
|
333
|
+
depth_weight = 1.0 / max_depth
|
|
334
|
+
|
|
335
|
+
# Normalized similarity
|
|
336
|
+
similarity = common_levels.to_f / max_depth
|
|
337
|
+
|
|
338
|
+
[similarity, depth_weight]
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Calculate similarity between two hierarchical tags (string version)
|
|
342
|
+
#
|
|
343
|
+
# Compares tags level by level, returning both similarity and a weight
|
|
344
|
+
# based on hierarchy depth (higher levels = more weight).
|
|
345
|
+
#
|
|
346
|
+
# @param tag_a [String] First tag (e.g., "database:postgresql:extensions")
|
|
347
|
+
# @param tag_b [String] Second tag (e.g., "database:postgresql:queries")
|
|
348
|
+
# @return [Array<Float, Float>] [similarity, weight] both in range 0.0-1.0
|
|
349
|
+
#
|
|
350
|
+
def calculate_hierarchical_similarity(tag_a, tag_b)
|
|
351
|
+
calculate_hierarchical_similarity_cached(tag_a.split(':'), tag_b.split(':'))
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class HTM
|
|
4
|
+
class LongTermMemory
|
|
5
|
+
# Robot registration and activity tracking
|
|
6
|
+
#
|
|
7
|
+
# Handles robot lifecycle management including:
|
|
8
|
+
# - Registration (find or create)
|
|
9
|
+
# - Activity timestamp updates
|
|
10
|
+
#
|
|
11
|
+
module RobotOperations
|
|
12
|
+
# Register a robot
|
|
13
|
+
#
|
|
14
|
+
# @param robot_name [String] Robot name
|
|
15
|
+
# @return [Integer] Robot ID
|
|
16
|
+
#
|
|
17
|
+
def register_robot(robot_name)
|
|
18
|
+
robot = HTM::Models::Robot.find_or_create_by(name: robot_name)
|
|
19
|
+
robot.update(last_active: Time.current)
|
|
20
|
+
robot.id
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Update robot activity timestamp
|
|
24
|
+
#
|
|
25
|
+
# @param robot_id [Integer] Robot identifier
|
|
26
|
+
# @return [void]
|
|
27
|
+
#
|
|
28
|
+
def update_robot_activity(robot_id)
|
|
29
|
+
robot = HTM::Models::Robot.find_by(id: robot_id)
|
|
30
|
+
robot&.update(last_active: Time.current)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|