htm 0.0.1 → 0.0.2
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/.envrc +1 -0
- data/.tbls.yml +30 -0
- data/CHANGELOG.md +30 -0
- data/SETUP.md +132 -101
- data/db/migrate/20250125000001_add_content_hash_to_nodes.rb +14 -0
- data/db/migrate/20250125000002_create_robot_nodes.rb +35 -0
- data/db/migrate/20250125000003_remove_source_and_robot_id_from_nodes.rb +28 -0
- data/db/migrate/20250126000001_create_working_memories.rb +19 -0
- data/db/migrate/20250126000002_remove_unused_columns.rb +12 -0
- data/db/schema.sql +226 -43
- data/docs/api/database.md +20 -232
- data/docs/api/embedding-service.md +1 -7
- data/docs/api/htm.md +195 -449
- data/docs/api/index.md +1 -7
- data/docs/api/long-term-memory.md +342 -590
- data/docs/architecture/adrs/001-postgresql-timescaledb.md +1 -1
- data/docs/architecture/adrs/003-ollama-embeddings.md +1 -1
- data/docs/architecture/adrs/010-redis-working-memory-rejected.md +2 -27
- data/docs/architecture/adrs/index.md +2 -13
- data/docs/architecture/hive-mind.md +165 -166
- data/docs/architecture/index.md +2 -2
- data/docs/architecture/overview.md +5 -171
- data/docs/architecture/two-tier-memory.md +1 -35
- data/docs/assets/images/adr-010-current-architecture.svg +37 -0
- data/docs/assets/images/adr-010-proposed-architecture.svg +48 -0
- data/docs/assets/images/adr-dependency-tree.svg +93 -0
- data/docs/assets/images/class-hierarchy.svg +55 -0
- data/docs/assets/images/exception-hierarchy.svg +45 -0
- data/docs/assets/images/htm-architecture-overview.svg +83 -0
- data/docs/assets/images/htm-complete-memory-flow.svg +160 -0
- data/docs/assets/images/htm-context-assembly-flow.svg +148 -0
- data/docs/assets/images/htm-eviction-process.svg +141 -0
- data/docs/assets/images/htm-memory-addition-flow.svg +138 -0
- data/docs/assets/images/htm-memory-recall-flow.svg +152 -0
- data/docs/assets/images/htm-node-states.svg +123 -0
- data/docs/assets/images/project-structure.svg +78 -0
- data/docs/assets/images/test-directory-structure.svg +38 -0
- data/{dbdoc → docs/database}/README.md +5 -3
- data/{dbdoc → docs/database}/public.node_tags.md +4 -5
- data/docs/database/public.node_tags.svg +106 -0
- data/{dbdoc → docs/database}/public.nodes.md +3 -8
- data/docs/database/public.nodes.svg +152 -0
- data/docs/database/public.robot_nodes.md +44 -0
- data/docs/database/public.robot_nodes.svg +121 -0
- data/{dbdoc → docs/database}/public.robots.md +1 -2
- data/docs/database/public.robots.svg +106 -0
- data/docs/database/public.working_memories.md +40 -0
- data/docs/database/public.working_memories.svg +112 -0
- data/{dbdoc → docs/database}/schema.json +342 -110
- data/docs/database/schema.svg +223 -0
- data/docs/development/index.md +1 -29
- data/docs/development/schema.md +84 -324
- data/docs/development/testing.md +1 -9
- data/docs/getting-started/index.md +47 -0
- data/docs/{installation.md → getting-started/installation.md} +2 -2
- data/docs/{quick-start.md → getting-started/quick-start.md} +5 -5
- data/docs/guides/adding-memories.md +221 -655
- data/docs/guides/search-strategies.md +85 -51
- data/docs/images/htm-er-diagram.svg +156 -0
- data/docs/index.md +16 -31
- data/docs/multi_framework_support.md +4 -4
- data/examples/basic_usage.rb +18 -16
- data/examples/cli_app/htm_cli.rb +86 -8
- data/examples/custom_llm_configuration.rb +1 -2
- data/examples/example_app/app.rb +11 -14
- data/examples/sinatra_app/Gemfile +1 -0
- data/examples/sinatra_app/Gemfile.lock +166 -0
- data/examples/sinatra_app/app.rb +219 -24
- data/lib/htm/active_record_config.rb +10 -3
- data/lib/htm/configuration.rb +265 -78
- data/lib/htm/{sinatra.rb → integrations/sinatra.rb} +87 -12
- data/lib/htm/job_adapter.rb +10 -3
- data/lib/htm/long_term_memory.rb +220 -57
- data/lib/htm/models/node.rb +36 -7
- data/lib/htm/models/robot.rb +30 -4
- data/lib/htm/models/robot_node.rb +50 -0
- data/lib/htm/models/tag.rb +52 -0
- data/lib/htm/models/working_memory_entry.rb +88 -0
- data/lib/htm/tasks.rb +4 -0
- data/lib/htm/version.rb +1 -1
- data/lib/htm.rb +34 -13
- data/lib/tasks/htm.rake +32 -1
- data/lib/tasks/jobs.rake +7 -3
- data/lib/tasks/tags.rake +34 -0
- data/mkdocs.yml +56 -9
- metadata +61 -31
- data/dbdoc/public.node_tags.svg +0 -112
- data/dbdoc/public.nodes.svg +0 -118
- data/dbdoc/public.robots.svg +0 -90
- data/dbdoc/schema.svg +0 -154
- /data/{dbdoc → docs/database}/public.node_stats.md +0 -0
- /data/{dbdoc → docs/database}/public.node_stats.svg +0 -0
- /data/{dbdoc → docs/database}/public.nodes_tags.md +0 -0
- /data/{dbdoc → docs/database}/public.nodes_tags.svg +0 -0
- /data/{dbdoc → docs/database}/public.ontology_structure.md +0 -0
- /data/{dbdoc → docs/database}/public.ontology_structure.svg +0 -0
- /data/{dbdoc → docs/database}/public.operations_log.md +0 -0
- /data/{dbdoc → docs/database}/public.operations_log.svg +0 -0
- /data/{dbdoc → docs/database}/public.relationships.md +0 -0
- /data/{dbdoc → docs/database}/public.relationships.svg +0 -0
- /data/{dbdoc → docs/database}/public.robot_activity.md +0 -0
- /data/{dbdoc → docs/database}/public.robot_activity.svg +0 -0
- /data/{dbdoc → docs/database}/public.schema_migrations.md +0 -0
- /data/{dbdoc → docs/database}/public.schema_migrations.svg +0 -0
- /data/{dbdoc → docs/database}/public.tags.md +0 -0
- /data/{dbdoc → docs/database}/public.tags.svg +0 -0
- /data/{dbdoc → docs/database}/public.topic_relationships.md +0 -0
- /data/{dbdoc → docs/database}/public.topic_relationships.svg +0 -0
data/examples/sinatra_app/app.rb
CHANGED
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
require 'sinatra'
|
|
23
23
|
require 'sinatra/json'
|
|
24
24
|
require 'sidekiq'
|
|
25
|
+
require 'securerandom'
|
|
25
26
|
require_relative '../../lib/htm'
|
|
26
|
-
require_relative '../../lib/htm/sinatra'
|
|
27
|
+
require_relative '../../lib/htm/integrations/sinatra'
|
|
27
28
|
|
|
28
29
|
# Sidekiq configuration
|
|
29
30
|
Sidekiq.configure_server do |config|
|
|
@@ -41,7 +42,11 @@ class HTMApp < Sinatra::Base
|
|
|
41
42
|
|
|
42
43
|
# Enable sessions for robot identification
|
|
43
44
|
enable :sessions
|
|
44
|
-
|
|
45
|
+
# Session secret must be at least 64 bytes for Rack session encryption
|
|
46
|
+
set :session_secret, ENV.fetch('SESSION_SECRET', SecureRandom.hex(64))
|
|
47
|
+
|
|
48
|
+
# Enable inline templates (defined after __END__)
|
|
49
|
+
enable :inline_templates
|
|
45
50
|
|
|
46
51
|
# Initialize HTM for each request
|
|
47
52
|
before do
|
|
@@ -66,7 +71,7 @@ class HTMApp < Sinatra::Base
|
|
|
66
71
|
halt 400, json(error: 'Content is required')
|
|
67
72
|
end
|
|
68
73
|
|
|
69
|
-
node_id = remember(content
|
|
74
|
+
node_id = remember(content)
|
|
70
75
|
|
|
71
76
|
json(
|
|
72
77
|
status: 'ok',
|
|
@@ -80,6 +85,7 @@ class HTMApp < Sinatra::Base
|
|
|
80
85
|
topic = params[:topic]
|
|
81
86
|
limit = (params[:limit] || 10).to_i
|
|
82
87
|
strategy = (params[:strategy] || 'hybrid').to_sym
|
|
88
|
+
timeframe_param = params[:timeframe]
|
|
83
89
|
|
|
84
90
|
unless topic && !topic.empty?
|
|
85
91
|
halt 400, json(error: 'Topic is required')
|
|
@@ -89,11 +95,16 @@ class HTMApp < Sinatra::Base
|
|
|
89
95
|
halt 400, json(error: 'Invalid strategy. Use: vector, fulltext, or hybrid')
|
|
90
96
|
end
|
|
91
97
|
|
|
92
|
-
|
|
98
|
+
# Parse timeframe parameter (in seconds)
|
|
99
|
+
# Valid values: "5", "10", "15", "20", "25", "30", "30+", "all", or nil
|
|
100
|
+
timeframe = parse_timeframe_param(timeframe_param)
|
|
101
|
+
|
|
102
|
+
memories = recall(topic, limit: limit, strategy: strategy, timeframe: timeframe, raw: true)
|
|
93
103
|
|
|
94
104
|
json(
|
|
95
105
|
status: 'ok',
|
|
96
106
|
count: memories.length,
|
|
107
|
+
timeframe: timeframe_param || 'all',
|
|
97
108
|
memories: memories.map { |m| format_memory(m) }
|
|
98
109
|
)
|
|
99
110
|
end
|
|
@@ -105,7 +116,7 @@ class HTMApp < Sinatra::Base
|
|
|
105
116
|
nodes_with_tags = HTM::Models::Node.joins(:tags).distinct.count
|
|
106
117
|
total_tags = HTM::Models::Tag.count
|
|
107
118
|
|
|
108
|
-
robot_nodes = HTM::Models::
|
|
119
|
+
robot_nodes = HTM::Models::RobotNode.where(robot_id: htm.robot_id).count
|
|
109
120
|
|
|
110
121
|
json(
|
|
111
122
|
status: 'ok',
|
|
@@ -133,16 +144,52 @@ class HTMApp < Sinatra::Base
|
|
|
133
144
|
)
|
|
134
145
|
end
|
|
135
146
|
|
|
147
|
+
# API: Get all tags as a tree structure
|
|
148
|
+
get '/api/tags' do
|
|
149
|
+
tags = HTM::Models::Tag.all
|
|
150
|
+
|
|
151
|
+
json(
|
|
152
|
+
status: 'ok',
|
|
153
|
+
count: tags.count,
|
|
154
|
+
tree: tags.tree
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
136
158
|
private
|
|
137
159
|
|
|
160
|
+
# Parse timeframe parameter from query string
|
|
161
|
+
# Returns a string like "last N seconds" or nil for "all"
|
|
162
|
+
def parse_timeframe_param(param)
|
|
163
|
+
return nil if param.nil? || param.empty? || param == 'all'
|
|
164
|
+
|
|
165
|
+
case param
|
|
166
|
+
when '5', '10', '15', '20', '25', '30'
|
|
167
|
+
"last #{param} seconds"
|
|
168
|
+
when '30+'
|
|
169
|
+
# 30+ means older than 30 seconds (from beginning of time to 30 seconds ago)
|
|
170
|
+
thirty_seconds_ago = Time.now - 30
|
|
171
|
+
Time.at(0)..thirty_seconds_ago
|
|
172
|
+
else
|
|
173
|
+
nil # Default to all time
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
138
177
|
def format_memory(memory)
|
|
139
|
-
{
|
|
178
|
+
result = {
|
|
140
179
|
id: memory['id'],
|
|
141
180
|
content: memory['content'],
|
|
142
|
-
source: memory['source'],
|
|
143
181
|
created_at: memory['created_at'],
|
|
144
182
|
token_count: memory['token_count']
|
|
145
183
|
}
|
|
184
|
+
|
|
185
|
+
# Include hybrid search scoring if available
|
|
186
|
+
if memory['similarity']
|
|
187
|
+
result[:similarity] = memory['similarity'].to_f.round(4)
|
|
188
|
+
result[:tag_boost] = memory['tag_boost'].to_f.round(4)
|
|
189
|
+
result[:combined_score] = memory['combined_score'].to_f.round(4)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
result
|
|
146
193
|
end
|
|
147
194
|
|
|
148
195
|
# Run the app
|
|
@@ -201,11 +248,40 @@ __END__
|
|
|
201
248
|
border-left-color: #dc3545;
|
|
202
249
|
background: #fff5f5;
|
|
203
250
|
}
|
|
251
|
+
.scores {
|
|
252
|
+
color: #6c757d;
|
|
253
|
+
font-style: italic;
|
|
254
|
+
}
|
|
255
|
+
.tag-tree {
|
|
256
|
+
font-family: monospace;
|
|
257
|
+
margin: 10px 0;
|
|
258
|
+
line-height: 1.4;
|
|
259
|
+
}
|
|
260
|
+
.filter-row {
|
|
261
|
+
display: flex;
|
|
262
|
+
gap: 10px;
|
|
263
|
+
margin: 10px 0;
|
|
264
|
+
}
|
|
265
|
+
.filter-row select {
|
|
266
|
+
flex: 1;
|
|
267
|
+
padding: 10px;
|
|
268
|
+
border: 1px solid #ddd;
|
|
269
|
+
border-radius: 4px;
|
|
270
|
+
}
|
|
271
|
+
.timeframe-badge {
|
|
272
|
+
display: inline-block;
|
|
273
|
+
background: #6c757d;
|
|
274
|
+
color: white;
|
|
275
|
+
padding: 2px 8px;
|
|
276
|
+
border-radius: 4px;
|
|
277
|
+
font-size: 0.85em;
|
|
278
|
+
margin-left: 8px;
|
|
279
|
+
}
|
|
204
280
|
</style>
|
|
205
281
|
</head>
|
|
206
282
|
<body>
|
|
207
283
|
<h1>HTM Sinatra Example</h1>
|
|
208
|
-
<p>Hierarchical Temporary Memory with Sidekiq background jobs</p>
|
|
284
|
+
<p>Hierarchical Temporary Memory with tag-enhanced hybrid search and Sidekiq background jobs</p>
|
|
209
285
|
|
|
210
286
|
<div class="section">
|
|
211
287
|
<h2>Remember Information</h2>
|
|
@@ -216,12 +292,25 @@ __END__
|
|
|
216
292
|
|
|
217
293
|
<div class="section">
|
|
218
294
|
<h2>Recall Memories</h2>
|
|
295
|
+
<p><small>Hybrid search uses combined scoring: (similarity × 0.7) + (tag_boost × 0.3)</small></p>
|
|
219
296
|
<input type="text" id="recallTopic" placeholder="Enter topic to search...">
|
|
220
|
-
<
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
297
|
+
<div class="filter-row">
|
|
298
|
+
<select id="recallStrategy">
|
|
299
|
+
<option value="hybrid">Hybrid (Vector + Fulltext + Tags)</option>
|
|
300
|
+
<option value="vector">Vector Only</option>
|
|
301
|
+
<option value="fulltext">Fulltext Only</option>
|
|
302
|
+
</select>
|
|
303
|
+
<select id="recallTimeframe">
|
|
304
|
+
<option value="all">All Time</option>
|
|
305
|
+
<option value="5">Last 5 seconds</option>
|
|
306
|
+
<option value="10">Last 10 seconds</option>
|
|
307
|
+
<option value="15">Last 15 seconds</option>
|
|
308
|
+
<option value="20">Last 20 seconds</option>
|
|
309
|
+
<option value="25">Last 25 seconds</option>
|
|
310
|
+
<option value="30">Last 30 seconds</option>
|
|
311
|
+
<option value="30+">Older than 30 seconds</option>
|
|
312
|
+
</select>
|
|
313
|
+
</div>
|
|
225
314
|
<button onclick="recall()">Recall</button>
|
|
226
315
|
<div id="recallResult"></div>
|
|
227
316
|
</div>
|
|
@@ -232,6 +321,12 @@ __END__
|
|
|
232
321
|
<div id="statsResult"></div>
|
|
233
322
|
</div>
|
|
234
323
|
|
|
324
|
+
<div class="section">
|
|
325
|
+
<h2>Tag Tree</h2>
|
|
326
|
+
<button onclick="getTags()">Refresh</button>
|
|
327
|
+
<div id="tagsResult"></div>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
235
330
|
<script>
|
|
236
331
|
async function remember() {
|
|
237
332
|
const content = document.getElementById('rememberContent').value;
|
|
@@ -268,6 +363,7 @@ __END__
|
|
|
268
363
|
async function recall() {
|
|
269
364
|
const topic = document.getElementById('recallTopic').value;
|
|
270
365
|
const strategy = document.getElementById('recallStrategy').value;
|
|
366
|
+
const timeframe = document.getElementById('recallTimeframe').value;
|
|
271
367
|
const resultDiv = document.getElementById('recallResult');
|
|
272
368
|
|
|
273
369
|
if (!topic) {
|
|
@@ -276,21 +372,33 @@ __END__
|
|
|
276
372
|
}
|
|
277
373
|
|
|
278
374
|
try {
|
|
279
|
-
const response = await fetch(`/api/recall?topic=${encodeURIComponent(topic)}&strategy=${strategy}&limit=10`);
|
|
375
|
+
const response = await fetch(`/api/recall?topic=${encodeURIComponent(topic)}&strategy=${strategy}&timeframe=${timeframe}&limit=10`);
|
|
280
376
|
const data = await response.json();
|
|
281
377
|
|
|
282
378
|
if (response.ok) {
|
|
283
379
|
if (data.count === 0) {
|
|
284
380
|
resultDiv.innerHTML = '<div class="result">No memories found</div>';
|
|
285
381
|
} else {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
382
|
+
// Format timeframe for display
|
|
383
|
+
const timeframeDisplay = formatTimeframe(data.timeframe);
|
|
384
|
+
const memoriesHtml = data.memories.map(m => {
|
|
385
|
+
// Build scoring info if available (hybrid search)
|
|
386
|
+
let scoreInfo = '';
|
|
387
|
+
if (m.combined_score !== undefined) {
|
|
388
|
+
scoreInfo = `<br><small class="scores">Score: ${m.combined_score.toFixed(3)} (similarity: ${m.similarity.toFixed(3)}, tag boost: ${m.tag_boost.toFixed(3)})</small>`;
|
|
389
|
+
}
|
|
390
|
+
// Calculate age of memory
|
|
391
|
+
const age = formatAge(m.created_at);
|
|
392
|
+
return `
|
|
393
|
+
<div class="result">
|
|
394
|
+
<strong>Node ${m.id}</strong> <span class="timeframe-badge">${age}</span><br>
|
|
395
|
+
${m.content}<br>
|
|
396
|
+
<small>${new Date(m.created_at).toLocaleString()} • ${m.token_count} tokens</small>
|
|
397
|
+
${scoreInfo}
|
|
398
|
+
</div>
|
|
399
|
+
`;
|
|
400
|
+
}).join('');
|
|
401
|
+
resultDiv.innerHTML = `<p>Found ${data.count} memories (${timeframeDisplay}):</p>${memoriesHtml}`;
|
|
294
402
|
}
|
|
295
403
|
} else {
|
|
296
404
|
resultDiv.innerHTML = `<div class="result error">✗ ${data.error}</div>`;
|
|
@@ -300,6 +408,36 @@ __END__
|
|
|
300
408
|
}
|
|
301
409
|
}
|
|
302
410
|
|
|
411
|
+
function formatTimeframe(tf) {
|
|
412
|
+
switch(tf) {
|
|
413
|
+
case 'all': return 'all time';
|
|
414
|
+
case '5': return 'last 5 seconds';
|
|
415
|
+
case '10': return 'last 10 seconds';
|
|
416
|
+
case '15': return 'last 15 seconds';
|
|
417
|
+
case '20': return 'last 20 seconds';
|
|
418
|
+
case '25': return 'last 25 seconds';
|
|
419
|
+
case '30': return 'last 30 seconds';
|
|
420
|
+
case '30+': return 'older than 30 seconds';
|
|
421
|
+
default: return tf;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function formatAge(createdAt) {
|
|
426
|
+
const now = new Date();
|
|
427
|
+
const created = new Date(createdAt);
|
|
428
|
+
const diffSeconds = Math.floor((now - created) / 1000);
|
|
429
|
+
|
|
430
|
+
if (diffSeconds < 60) {
|
|
431
|
+
return `${diffSeconds}s ago`;
|
|
432
|
+
} else if (diffSeconds < 3600) {
|
|
433
|
+
return `${Math.floor(diffSeconds / 60)}m ago`;
|
|
434
|
+
} else if (diffSeconds < 86400) {
|
|
435
|
+
return `${Math.floor(diffSeconds / 3600)}h ago`;
|
|
436
|
+
} else {
|
|
437
|
+
return `${Math.floor(diffSeconds / 86400)}d ago`;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
303
441
|
async function getStats() {
|
|
304
442
|
const resultDiv = document.getElementById('statsResult');
|
|
305
443
|
|
|
@@ -328,8 +466,65 @@ __END__
|
|
|
328
466
|
}
|
|
329
467
|
}
|
|
330
468
|
|
|
331
|
-
|
|
332
|
-
|
|
469
|
+
async function getTags() {
|
|
470
|
+
const resultDiv = document.getElementById('tagsResult');
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
const response = await fetch('/api/tags');
|
|
474
|
+
const data = await response.json();
|
|
475
|
+
|
|
476
|
+
if (response.ok) {
|
|
477
|
+
if (data.count === 0) {
|
|
478
|
+
resultDiv.innerHTML = '<div class="result">No tags found</div>';
|
|
479
|
+
} else {
|
|
480
|
+
const treeHtml = renderTagTree(data.tree);
|
|
481
|
+
resultDiv.innerHTML = `
|
|
482
|
+
<div class="result">
|
|
483
|
+
<pre class="tag-tree">${treeHtml}</pre>
|
|
484
|
+
<small>${data.count} tags</small>
|
|
485
|
+
</div>
|
|
486
|
+
`;
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
resultDiv.innerHTML = `<div class="result error">✗ ${data.error}</div>`;
|
|
490
|
+
}
|
|
491
|
+
} catch (error) {
|
|
492
|
+
resultDiv.innerHTML = `<div class="result error">✗ ${error.message}</div>`;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function renderTagTree(tree, prefix = '', isLastArray = []) {
|
|
497
|
+
const keys = Object.keys(tree).sort();
|
|
498
|
+
let result = '';
|
|
499
|
+
|
|
500
|
+
keys.forEach((key, index) => {
|
|
501
|
+
const isLast = index === keys.length - 1;
|
|
502
|
+
|
|
503
|
+
// Build prefix from parent branches
|
|
504
|
+
let linePrefix = '';
|
|
505
|
+
isLastArray.forEach(wasLast => {
|
|
506
|
+
linePrefix += wasLast ? ' ' : '│ ';
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Add branch character
|
|
510
|
+
const branch = isLast ? '└── ' : '├── ';
|
|
511
|
+
result += linePrefix + branch + key + '\n';
|
|
512
|
+
|
|
513
|
+
// Recurse into children
|
|
514
|
+
const children = tree[key];
|
|
515
|
+
if (Object.keys(children).length > 0) {
|
|
516
|
+
result += renderTagTree(children, prefix, [...isLastArray, isLast]);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
return result;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Load stats and tags on page load
|
|
524
|
+
window.onload = function() {
|
|
525
|
+
getStats();
|
|
526
|
+
getTags();
|
|
527
|
+
};
|
|
333
528
|
</script>
|
|
334
529
|
</body>
|
|
335
530
|
</html>
|
|
@@ -53,9 +53,14 @@ class HTM
|
|
|
53
53
|
|
|
54
54
|
# Check if connection is established and active
|
|
55
55
|
def connected?
|
|
56
|
-
ActiveRecord::Base
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
return false unless defined?(ActiveRecord::Base)
|
|
57
|
+
return false unless ActiveRecord::Base.connection_handler.connection_pool_list.any?
|
|
58
|
+
|
|
59
|
+
ActiveRecord::Base.connected? && ActiveRecord::Base.connection.active?
|
|
60
|
+
rescue ActiveRecord::ConnectionNotDefined, ActiveRecord::ConnectionNotEstablished
|
|
61
|
+
false
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
HTM.logger.debug "Connection check failed: #{e.class} - #{e.message}"
|
|
59
64
|
false
|
|
60
65
|
end
|
|
61
66
|
|
|
@@ -105,8 +110,10 @@ class HTM
|
|
|
105
110
|
def require_models
|
|
106
111
|
require_relative 'models/robot'
|
|
107
112
|
require_relative 'models/node'
|
|
113
|
+
require_relative 'models/robot_node'
|
|
108
114
|
require_relative 'models/tag'
|
|
109
115
|
require_relative 'models/node_tag'
|
|
116
|
+
require_relative 'models/working_memory_entry'
|
|
110
117
|
end
|
|
111
118
|
end
|
|
112
119
|
end
|