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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.tbls.yml +30 -0
  4. data/CHANGELOG.md +30 -0
  5. data/SETUP.md +132 -101
  6. data/db/migrate/20250125000001_add_content_hash_to_nodes.rb +14 -0
  7. data/db/migrate/20250125000002_create_robot_nodes.rb +35 -0
  8. data/db/migrate/20250125000003_remove_source_and_robot_id_from_nodes.rb +28 -0
  9. data/db/migrate/20250126000001_create_working_memories.rb +19 -0
  10. data/db/migrate/20250126000002_remove_unused_columns.rb +12 -0
  11. data/db/schema.sql +226 -43
  12. data/docs/api/database.md +20 -232
  13. data/docs/api/embedding-service.md +1 -7
  14. data/docs/api/htm.md +195 -449
  15. data/docs/api/index.md +1 -7
  16. data/docs/api/long-term-memory.md +342 -590
  17. data/docs/architecture/adrs/001-postgresql-timescaledb.md +1 -1
  18. data/docs/architecture/adrs/003-ollama-embeddings.md +1 -1
  19. data/docs/architecture/adrs/010-redis-working-memory-rejected.md +2 -27
  20. data/docs/architecture/adrs/index.md +2 -13
  21. data/docs/architecture/hive-mind.md +165 -166
  22. data/docs/architecture/index.md +2 -2
  23. data/docs/architecture/overview.md +5 -171
  24. data/docs/architecture/two-tier-memory.md +1 -35
  25. data/docs/assets/images/adr-010-current-architecture.svg +37 -0
  26. data/docs/assets/images/adr-010-proposed-architecture.svg +48 -0
  27. data/docs/assets/images/adr-dependency-tree.svg +93 -0
  28. data/docs/assets/images/class-hierarchy.svg +55 -0
  29. data/docs/assets/images/exception-hierarchy.svg +45 -0
  30. data/docs/assets/images/htm-architecture-overview.svg +83 -0
  31. data/docs/assets/images/htm-complete-memory-flow.svg +160 -0
  32. data/docs/assets/images/htm-context-assembly-flow.svg +148 -0
  33. data/docs/assets/images/htm-eviction-process.svg +141 -0
  34. data/docs/assets/images/htm-memory-addition-flow.svg +138 -0
  35. data/docs/assets/images/htm-memory-recall-flow.svg +152 -0
  36. data/docs/assets/images/htm-node-states.svg +123 -0
  37. data/docs/assets/images/project-structure.svg +78 -0
  38. data/docs/assets/images/test-directory-structure.svg +38 -0
  39. data/{dbdoc → docs/database}/README.md +5 -3
  40. data/{dbdoc → docs/database}/public.node_tags.md +4 -5
  41. data/docs/database/public.node_tags.svg +106 -0
  42. data/{dbdoc → docs/database}/public.nodes.md +3 -8
  43. data/docs/database/public.nodes.svg +152 -0
  44. data/docs/database/public.robot_nodes.md +44 -0
  45. data/docs/database/public.robot_nodes.svg +121 -0
  46. data/{dbdoc → docs/database}/public.robots.md +1 -2
  47. data/docs/database/public.robots.svg +106 -0
  48. data/docs/database/public.working_memories.md +40 -0
  49. data/docs/database/public.working_memories.svg +112 -0
  50. data/{dbdoc → docs/database}/schema.json +342 -110
  51. data/docs/database/schema.svg +223 -0
  52. data/docs/development/index.md +1 -29
  53. data/docs/development/schema.md +84 -324
  54. data/docs/development/testing.md +1 -9
  55. data/docs/getting-started/index.md +47 -0
  56. data/docs/{installation.md → getting-started/installation.md} +2 -2
  57. data/docs/{quick-start.md → getting-started/quick-start.md} +5 -5
  58. data/docs/guides/adding-memories.md +221 -655
  59. data/docs/guides/search-strategies.md +85 -51
  60. data/docs/images/htm-er-diagram.svg +156 -0
  61. data/docs/index.md +16 -31
  62. data/docs/multi_framework_support.md +4 -4
  63. data/examples/basic_usage.rb +18 -16
  64. data/examples/cli_app/htm_cli.rb +86 -8
  65. data/examples/custom_llm_configuration.rb +1 -2
  66. data/examples/example_app/app.rb +11 -14
  67. data/examples/sinatra_app/Gemfile +1 -0
  68. data/examples/sinatra_app/Gemfile.lock +166 -0
  69. data/examples/sinatra_app/app.rb +219 -24
  70. data/lib/htm/active_record_config.rb +10 -3
  71. data/lib/htm/configuration.rb +265 -78
  72. data/lib/htm/{sinatra.rb → integrations/sinatra.rb} +87 -12
  73. data/lib/htm/job_adapter.rb +10 -3
  74. data/lib/htm/long_term_memory.rb +220 -57
  75. data/lib/htm/models/node.rb +36 -7
  76. data/lib/htm/models/robot.rb +30 -4
  77. data/lib/htm/models/robot_node.rb +50 -0
  78. data/lib/htm/models/tag.rb +52 -0
  79. data/lib/htm/models/working_memory_entry.rb +88 -0
  80. data/lib/htm/tasks.rb +4 -0
  81. data/lib/htm/version.rb +1 -1
  82. data/lib/htm.rb +34 -13
  83. data/lib/tasks/htm.rake +32 -1
  84. data/lib/tasks/jobs.rake +7 -3
  85. data/lib/tasks/tags.rake +34 -0
  86. data/mkdocs.yml +56 -9
  87. metadata +61 -31
  88. data/dbdoc/public.node_tags.svg +0 -112
  89. data/dbdoc/public.nodes.svg +0 -118
  90. data/dbdoc/public.robots.svg +0 -90
  91. data/dbdoc/schema.svg +0 -154
  92. /data/{dbdoc → docs/database}/public.node_stats.md +0 -0
  93. /data/{dbdoc → docs/database}/public.node_stats.svg +0 -0
  94. /data/{dbdoc → docs/database}/public.nodes_tags.md +0 -0
  95. /data/{dbdoc → docs/database}/public.nodes_tags.svg +0 -0
  96. /data/{dbdoc → docs/database}/public.ontology_structure.md +0 -0
  97. /data/{dbdoc → docs/database}/public.ontology_structure.svg +0 -0
  98. /data/{dbdoc → docs/database}/public.operations_log.md +0 -0
  99. /data/{dbdoc → docs/database}/public.operations_log.svg +0 -0
  100. /data/{dbdoc → docs/database}/public.relationships.md +0 -0
  101. /data/{dbdoc → docs/database}/public.relationships.svg +0 -0
  102. /data/{dbdoc → docs/database}/public.robot_activity.md +0 -0
  103. /data/{dbdoc → docs/database}/public.robot_activity.svg +0 -0
  104. /data/{dbdoc → docs/database}/public.schema_migrations.md +0 -0
  105. /data/{dbdoc → docs/database}/public.schema_migrations.svg +0 -0
  106. /data/{dbdoc → docs/database}/public.tags.md +0 -0
  107. /data/{dbdoc → docs/database}/public.tags.svg +0 -0
  108. /data/{dbdoc → docs/database}/public.topic_relationships.md +0 -0
  109. /data/{dbdoc → docs/database}/public.topic_relationships.svg +0 -0
@@ -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
- set :session_secret, ENV.fetch('SESSION_SECRET', 'change_me_in_production')
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, source: 'web_user')
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
- memories = recall(topic, limit: limit, strategy: strategy, raw: true)
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::Node.where(robot_id: htm.robot_id).count
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
- <select id="recallStrategy">
221
- <option value="hybrid">Hybrid (Vector + Fulltext)</option>
222
- <option value="vector">Vector Only</option>
223
- <option value="fulltext">Fulltext Only</option>
224
- </select>
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
- const memoriesHtml = data.memories.map(m => `
287
- <div class="result">
288
- <strong>Node ${m.id}</strong> (${m.source})<br>
289
- ${m.content}<br>
290
- <small>${new Date(m.created_at).toLocaleString()} ${m.token_count} tokens</small>
291
- </div>
292
- `).join('');
293
- resultDiv.innerHTML = `<p>Found ${data.count} memories:</p>${memoriesHtml}`;
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
- // Load stats on page load
332
- window.onload = getStats;
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.connected? &&
57
- ActiveRecord::Base.connection.active?
58
- rescue StandardError
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