htm 0.0.1

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 (155) hide show
  1. checksums.yaml +7 -0
  2. data/.architecture/decisions/adrs/001-use-postgresql-timescaledb-storage.md +227 -0
  3. data/.architecture/decisions/adrs/002-two-tier-memory-architecture.md +322 -0
  4. data/.architecture/decisions/adrs/003-ollama-default-embedding-provider.md +339 -0
  5. data/.architecture/decisions/adrs/004-multi-robot-shared-memory-hive-mind.md +374 -0
  6. data/.architecture/decisions/adrs/005-rag-based-retrieval-with-hybrid-search.md +443 -0
  7. data/.architecture/decisions/adrs/006-context-assembly-strategies.md +444 -0
  8. data/.architecture/decisions/adrs/007-working-memory-eviction-strategy.md +461 -0
  9. data/.architecture/decisions/adrs/008-robot-identification-system.md +550 -0
  10. data/.architecture/decisions/adrs/009-never-forget-explicit-deletion-only.md +570 -0
  11. data/.architecture/decisions/adrs/010-redis-working-memory-rejected.md +323 -0
  12. data/.architecture/decisions/adrs/011-database-side-embedding-generation-with-pgai.md +585 -0
  13. data/.architecture/decisions/adrs/012-llm-driven-ontology-topic-extraction.md +583 -0
  14. data/.architecture/decisions/adrs/013-activerecord-orm-and-many-to-many-tagging.md +299 -0
  15. data/.architecture/decisions/adrs/014-client-side-embedding-generation-workflow.md +569 -0
  16. data/.architecture/decisions/adrs/015-hierarchical-tag-ontology-and-llm-extraction.md +701 -0
  17. data/.architecture/decisions/adrs/016-async-embedding-and-tag-generation.md +694 -0
  18. data/.architecture/members.yml +144 -0
  19. data/.architecture/reviews/2025-10-29-llm-configuration-and-async-processing-review.md +1137 -0
  20. data/.architecture/reviews/initial-system-analysis.md +330 -0
  21. data/.envrc +32 -0
  22. data/.irbrc +145 -0
  23. data/CHANGELOG.md +150 -0
  24. data/COMMITS.md +196 -0
  25. data/LICENSE +21 -0
  26. data/README.md +1347 -0
  27. data/Rakefile +51 -0
  28. data/SETUP.md +268 -0
  29. data/config/database.yml +67 -0
  30. data/db/migrate/20250101000001_enable_extensions.rb +14 -0
  31. data/db/migrate/20250101000002_create_robots.rb +14 -0
  32. data/db/migrate/20250101000003_create_nodes.rb +42 -0
  33. data/db/migrate/20250101000005_create_tags.rb +38 -0
  34. data/db/migrate/20250101000007_add_node_vector_indexes.rb +30 -0
  35. data/db/schema.sql +473 -0
  36. data/db/seed_data/README.md +100 -0
  37. data/db/seed_data/presidents.md +136 -0
  38. data/db/seed_data/states.md +151 -0
  39. data/db/seeds.rb +208 -0
  40. data/dbdoc/README.md +173 -0
  41. data/dbdoc/public.node_stats.md +48 -0
  42. data/dbdoc/public.node_stats.svg +41 -0
  43. data/dbdoc/public.node_tags.md +40 -0
  44. data/dbdoc/public.node_tags.svg +112 -0
  45. data/dbdoc/public.nodes.md +54 -0
  46. data/dbdoc/public.nodes.svg +118 -0
  47. data/dbdoc/public.nodes_tags.md +39 -0
  48. data/dbdoc/public.nodes_tags.svg +112 -0
  49. data/dbdoc/public.ontology_structure.md +48 -0
  50. data/dbdoc/public.ontology_structure.svg +38 -0
  51. data/dbdoc/public.operations_log.md +42 -0
  52. data/dbdoc/public.operations_log.svg +130 -0
  53. data/dbdoc/public.relationships.md +39 -0
  54. data/dbdoc/public.relationships.svg +41 -0
  55. data/dbdoc/public.robot_activity.md +46 -0
  56. data/dbdoc/public.robot_activity.svg +35 -0
  57. data/dbdoc/public.robots.md +35 -0
  58. data/dbdoc/public.robots.svg +90 -0
  59. data/dbdoc/public.schema_migrations.md +29 -0
  60. data/dbdoc/public.schema_migrations.svg +26 -0
  61. data/dbdoc/public.tags.md +35 -0
  62. data/dbdoc/public.tags.svg +60 -0
  63. data/dbdoc/public.topic_relationships.md +45 -0
  64. data/dbdoc/public.topic_relationships.svg +32 -0
  65. data/dbdoc/schema.json +1437 -0
  66. data/dbdoc/schema.svg +154 -0
  67. data/docs/api/database.md +806 -0
  68. data/docs/api/embedding-service.md +532 -0
  69. data/docs/api/htm.md +797 -0
  70. data/docs/api/index.md +259 -0
  71. data/docs/api/long-term-memory.md +1096 -0
  72. data/docs/api/working-memory.md +665 -0
  73. data/docs/architecture/adrs/001-postgresql-timescaledb.md +314 -0
  74. data/docs/architecture/adrs/002-two-tier-memory.md +411 -0
  75. data/docs/architecture/adrs/003-ollama-embeddings.md +421 -0
  76. data/docs/architecture/adrs/004-hive-mind.md +437 -0
  77. data/docs/architecture/adrs/005-rag-retrieval.md +531 -0
  78. data/docs/architecture/adrs/006-context-assembly.md +496 -0
  79. data/docs/architecture/adrs/007-eviction-strategy.md +645 -0
  80. data/docs/architecture/adrs/008-robot-identification.md +625 -0
  81. data/docs/architecture/adrs/009-never-forget.md +648 -0
  82. data/docs/architecture/adrs/010-redis-working-memory-rejected.md +323 -0
  83. data/docs/architecture/adrs/011-pgai-integration.md +494 -0
  84. data/docs/architecture/adrs/index.md +215 -0
  85. data/docs/architecture/hive-mind.md +736 -0
  86. data/docs/architecture/index.md +351 -0
  87. data/docs/architecture/overview.md +538 -0
  88. data/docs/architecture/two-tier-memory.md +873 -0
  89. data/docs/assets/css/custom.css +83 -0
  90. data/docs/assets/images/htm-core-components.svg +63 -0
  91. data/docs/assets/images/htm-database-schema.svg +93 -0
  92. data/docs/assets/images/htm-hive-mind-architecture.svg +125 -0
  93. data/docs/assets/images/htm-importance-scoring-framework.svg +83 -0
  94. data/docs/assets/images/htm-layered-architecture.svg +71 -0
  95. data/docs/assets/images/htm-long-term-memory-architecture.svg +115 -0
  96. data/docs/assets/images/htm-working-memory-architecture.svg +120 -0
  97. data/docs/assets/images/htm.jpg +0 -0
  98. data/docs/assets/images/htm_demo.gif +0 -0
  99. data/docs/assets/js/mathjax.js +18 -0
  100. data/docs/assets/videos/htm_video.mp4 +0 -0
  101. data/docs/database_rake_tasks.md +322 -0
  102. data/docs/development/contributing.md +787 -0
  103. data/docs/development/index.md +336 -0
  104. data/docs/development/schema.md +596 -0
  105. data/docs/development/setup.md +719 -0
  106. data/docs/development/testing.md +819 -0
  107. data/docs/guides/adding-memories.md +824 -0
  108. data/docs/guides/context-assembly.md +1009 -0
  109. data/docs/guides/getting-started.md +577 -0
  110. data/docs/guides/index.md +118 -0
  111. data/docs/guides/long-term-memory.md +941 -0
  112. data/docs/guides/multi-robot.md +866 -0
  113. data/docs/guides/recalling-memories.md +927 -0
  114. data/docs/guides/search-strategies.md +953 -0
  115. data/docs/guides/working-memory.md +717 -0
  116. data/docs/index.md +214 -0
  117. data/docs/installation.md +477 -0
  118. data/docs/multi_framework_support.md +519 -0
  119. data/docs/quick-start.md +655 -0
  120. data/docs/setup_local_database.md +302 -0
  121. data/docs/using_rake_tasks_in_your_app.md +383 -0
  122. data/examples/basic_usage.rb +93 -0
  123. data/examples/cli_app/README.md +317 -0
  124. data/examples/cli_app/htm_cli.rb +270 -0
  125. data/examples/custom_llm_configuration.rb +183 -0
  126. data/examples/example_app/Rakefile +71 -0
  127. data/examples/example_app/app.rb +206 -0
  128. data/examples/sinatra_app/Gemfile +21 -0
  129. data/examples/sinatra_app/app.rb +335 -0
  130. data/lib/htm/active_record_config.rb +113 -0
  131. data/lib/htm/configuration.rb +342 -0
  132. data/lib/htm/database.rb +594 -0
  133. data/lib/htm/embedding_service.rb +115 -0
  134. data/lib/htm/errors.rb +34 -0
  135. data/lib/htm/job_adapter.rb +154 -0
  136. data/lib/htm/jobs/generate_embedding_job.rb +65 -0
  137. data/lib/htm/jobs/generate_tags_job.rb +82 -0
  138. data/lib/htm/long_term_memory.rb +965 -0
  139. data/lib/htm/models/node.rb +109 -0
  140. data/lib/htm/models/node_tag.rb +33 -0
  141. data/lib/htm/models/robot.rb +52 -0
  142. data/lib/htm/models/tag.rb +76 -0
  143. data/lib/htm/railtie.rb +76 -0
  144. data/lib/htm/sinatra.rb +157 -0
  145. data/lib/htm/tag_service.rb +135 -0
  146. data/lib/htm/tasks.rb +38 -0
  147. data/lib/htm/version.rb +5 -0
  148. data/lib/htm/working_memory.rb +182 -0
  149. data/lib/htm.rb +400 -0
  150. data/lib/tasks/db.rake +19 -0
  151. data/lib/tasks/htm.rake +147 -0
  152. data/lib/tasks/jobs.rake +312 -0
  153. data/mkdocs.yml +190 -0
  154. data/scripts/install_local_database.sh +309 -0
  155. metadata +341 -0
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # HTM Sinatra Application Example
5
+ #
6
+ # Demonstrates using HTM in a Sinatra web application with:
7
+ # - Sidekiq for background job processing
8
+ # - Session-based robot identification
9
+ # - RESTful API endpoints
10
+ # - Thread-safe concurrent request handling
11
+ #
12
+ # Usage:
13
+ # bundle install
14
+ # bundle exec ruby app.rb
15
+ #
16
+ # Environment:
17
+ # HTM_DBURL - PostgreSQL connection URL (required)
18
+ # REDIS_URL - Redis connection URL (for Sidekiq, default: redis://localhost:6379/0)
19
+ # OLLAMA_URL - Ollama server URL (default: http://localhost:11434)
20
+ #
21
+
22
+ require 'sinatra'
23
+ require 'sinatra/json'
24
+ require 'sidekiq'
25
+ require_relative '../../lib/htm'
26
+ require_relative '../../lib/htm/sinatra'
27
+
28
+ # Sidekiq configuration
29
+ Sidekiq.configure_server do |config|
30
+ config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0') }
31
+ end
32
+
33
+ Sidekiq.configure_client do |config|
34
+ config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0') }
35
+ end
36
+
37
+ # Sinatra Application
38
+ class HTMApp < Sinatra::Base
39
+ # Register HTM with automatic configuration
40
+ register_htm
41
+
42
+ # Enable sessions for robot identification
43
+ enable :sessions
44
+ set :session_secret, ENV.fetch('SESSION_SECRET', 'change_me_in_production')
45
+
46
+ # Initialize HTM for each request
47
+ before do
48
+ # Use session ID as robot identifier
49
+ robot_name = session[:robot_id] ||= SecureRandom.uuid[0..7]
50
+ init_htm(robot_name: "web_user_#{robot_name}")
51
+
52
+ # Set content type for API responses
53
+ content_type :json if request.path.start_with?('/api/')
54
+ end
55
+
56
+ # Home page
57
+ get '/' do
58
+ erb :index
59
+ end
60
+
61
+ # API: Remember information
62
+ post '/api/remember' do
63
+ content = params[:content]
64
+
65
+ unless content && !content.empty?
66
+ halt 400, json(error: 'Content is required')
67
+ end
68
+
69
+ node_id = remember(content, source: 'web_user')
70
+
71
+ json(
72
+ status: 'ok',
73
+ node_id: node_id,
74
+ message: 'Memory stored. Embedding and tags are being generated in background.'
75
+ )
76
+ end
77
+
78
+ # API: Recall memories
79
+ get '/api/recall' do
80
+ topic = params[:topic]
81
+ limit = (params[:limit] || 10).to_i
82
+ strategy = (params[:strategy] || 'hybrid').to_sym
83
+
84
+ unless topic && !topic.empty?
85
+ halt 400, json(error: 'Topic is required')
86
+ end
87
+
88
+ unless [:vector, :fulltext, :hybrid].include?(strategy)
89
+ halt 400, json(error: 'Invalid strategy. Use: vector, fulltext, or hybrid')
90
+ end
91
+
92
+ memories = recall(topic, limit: limit, strategy: strategy, raw: true)
93
+
94
+ json(
95
+ status: 'ok',
96
+ count: memories.length,
97
+ memories: memories.map { |m| format_memory(m) }
98
+ )
99
+ end
100
+
101
+ # API: Get memory statistics
102
+ get '/api/stats' do
103
+ total_nodes = HTM::Models::Node.count
104
+ nodes_with_embeddings = HTM::Models::Node.where.not(embedding: nil).count
105
+ nodes_with_tags = HTM::Models::Node.joins(:tags).distinct.count
106
+ total_tags = HTM::Models::Tag.count
107
+
108
+ robot_nodes = HTM::Models::Node.where(robot_id: htm.robot_id).count
109
+
110
+ json(
111
+ status: 'ok',
112
+ stats: {
113
+ total_nodes: total_nodes,
114
+ nodes_with_embeddings: nodes_with_embeddings,
115
+ nodes_with_tags: nodes_with_tags,
116
+ total_tags: total_tags,
117
+ current_robot: {
118
+ id: htm.robot_id,
119
+ name: htm.robot_name,
120
+ nodes: robot_nodes
121
+ }
122
+ }
123
+ )
124
+ end
125
+
126
+ # API: Health check
127
+ get '/api/health' do
128
+ json(
129
+ status: 'ok',
130
+ job_backend: HTM.configuration.job_backend,
131
+ database: HTM::ActiveRecordConfig.connected?,
132
+ timestamp: Time.now.iso8601
133
+ )
134
+ end
135
+
136
+ private
137
+
138
+ def format_memory(memory)
139
+ {
140
+ id: memory['id'],
141
+ content: memory['content'],
142
+ source: memory['source'],
143
+ created_at: memory['created_at'],
144
+ token_count: memory['token_count']
145
+ }
146
+ end
147
+
148
+ # Run the app
149
+ run! if app_file == $0
150
+ end
151
+
152
+ __END__
153
+
154
+ @@index
155
+ <!DOCTYPE html>
156
+ <html>
157
+ <head>
158
+ <title>HTM Sinatra Example</title>
159
+ <meta charset="utf-8">
160
+ <meta name="viewport" content="width=device-width, initial-scale=1">
161
+ <style>
162
+ body {
163
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
164
+ max-width: 800px;
165
+ margin: 40px auto;
166
+ padding: 0 20px;
167
+ line-height: 1.6;
168
+ }
169
+ h1 { color: #333; }
170
+ .section {
171
+ background: #f5f5f5;
172
+ padding: 20px;
173
+ margin: 20px 0;
174
+ border-radius: 8px;
175
+ }
176
+ input[type="text"], textarea {
177
+ width: 100%;
178
+ padding: 10px;
179
+ margin: 10px 0;
180
+ border: 1px solid #ddd;
181
+ border-radius: 4px;
182
+ box-sizing: border-box;
183
+ }
184
+ button {
185
+ background: #007bff;
186
+ color: white;
187
+ padding: 10px 20px;
188
+ border: none;
189
+ border-radius: 4px;
190
+ cursor: pointer;
191
+ }
192
+ button:hover { background: #0056b3; }
193
+ .result {
194
+ background: white;
195
+ padding: 15px;
196
+ margin: 10px 0;
197
+ border-radius: 4px;
198
+ border-left: 4px solid #007bff;
199
+ }
200
+ .error {
201
+ border-left-color: #dc3545;
202
+ background: #fff5f5;
203
+ }
204
+ </style>
205
+ </head>
206
+ <body>
207
+ <h1>HTM Sinatra Example</h1>
208
+ <p>Hierarchical Temporary Memory with Sidekiq background jobs</p>
209
+
210
+ <div class="section">
211
+ <h2>Remember Information</h2>
212
+ <textarea id="rememberContent" rows="4" placeholder="Enter information to remember..."></textarea>
213
+ <button onclick="remember()">Remember</button>
214
+ <div id="rememberResult"></div>
215
+ </div>
216
+
217
+ <div class="section">
218
+ <h2>Recall Memories</h2>
219
+ <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>
225
+ <button onclick="recall()">Recall</button>
226
+ <div id="recallResult"></div>
227
+ </div>
228
+
229
+ <div class="section">
230
+ <h2>Memory Statistics</h2>
231
+ <button onclick="getStats()">Refresh Stats</button>
232
+ <div id="statsResult"></div>
233
+ </div>
234
+
235
+ <script>
236
+ async function remember() {
237
+ const content = document.getElementById('rememberContent').value;
238
+ const resultDiv = document.getElementById('rememberResult');
239
+
240
+ if (!content) {
241
+ resultDiv.innerHTML = '<div class="result error">Please enter some content</div>';
242
+ return;
243
+ }
244
+
245
+ try {
246
+ const response = await fetch('/api/remember', {
247
+ method: 'POST',
248
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
249
+ body: `content=${encodeURIComponent(content)}`
250
+ });
251
+
252
+ const data = await response.json();
253
+
254
+ if (response.ok) {
255
+ resultDiv.innerHTML = `<div class="result">
256
+ ✓ ${data.message}<br>
257
+ Node ID: ${data.node_id}
258
+ </div>`;
259
+ document.getElementById('rememberContent').value = '';
260
+ } else {
261
+ resultDiv.innerHTML = `<div class="result error">✗ ${data.error}</div>`;
262
+ }
263
+ } catch (error) {
264
+ resultDiv.innerHTML = `<div class="result error">✗ ${error.message}</div>`;
265
+ }
266
+ }
267
+
268
+ async function recall() {
269
+ const topic = document.getElementById('recallTopic').value;
270
+ const strategy = document.getElementById('recallStrategy').value;
271
+ const resultDiv = document.getElementById('recallResult');
272
+
273
+ if (!topic) {
274
+ resultDiv.innerHTML = '<div class="result error">Please enter a topic</div>';
275
+ return;
276
+ }
277
+
278
+ try {
279
+ const response = await fetch(`/api/recall?topic=${encodeURIComponent(topic)}&strategy=${strategy}&limit=10`);
280
+ const data = await response.json();
281
+
282
+ if (response.ok) {
283
+ if (data.count === 0) {
284
+ resultDiv.innerHTML = '<div class="result">No memories found</div>';
285
+ } 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}`;
294
+ }
295
+ } else {
296
+ resultDiv.innerHTML = `<div class="result error">✗ ${data.error}</div>`;
297
+ }
298
+ } catch (error) {
299
+ resultDiv.innerHTML = `<div class="result error">✗ ${error.message}</div>`;
300
+ }
301
+ }
302
+
303
+ async function getStats() {
304
+ const resultDiv = document.getElementById('statsResult');
305
+
306
+ try {
307
+ const response = await fetch('/api/stats');
308
+ const data = await response.json();
309
+
310
+ if (response.ok) {
311
+ const stats = data.stats;
312
+ resultDiv.innerHTML = `
313
+ <div class="result">
314
+ <strong>Global Statistics:</strong><br>
315
+ Total Nodes: ${stats.total_nodes}<br>
316
+ With Embeddings: ${stats.nodes_with_embeddings}<br>
317
+ With Tags: ${stats.nodes_with_tags}<br>
318
+ Total Tags: ${stats.total_tags}<br><br>
319
+ <strong>Your Session (${stats.current_robot.name}):</strong><br>
320
+ Nodes: ${stats.current_robot.nodes}
321
+ </div>
322
+ `;
323
+ } else {
324
+ resultDiv.innerHTML = `<div class="result error">✗ ${data.error}</div>`;
325
+ }
326
+ } catch (error) {
327
+ resultDiv.innerHTML = `<div class="result error">✗ ${error.message}</div>`;
328
+ }
329
+ }
330
+
331
+ // Load stats on page load
332
+ window.onload = getStats;
333
+ </script>
334
+ </body>
335
+ </html>
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require 'pg'
5
+ require 'neighbor'
6
+ require 'erb'
7
+ require 'yaml'
8
+
9
+ class HTM
10
+ # ActiveRecord database configuration and model loading
11
+ class ActiveRecordConfig
12
+ class << self
13
+ # Establish database connection from config/database.yml
14
+ def establish_connection!
15
+ config = load_database_config
16
+
17
+ ActiveRecord::Base.establish_connection(config)
18
+
19
+ # Set search path
20
+ ActiveRecord::Base.connection.execute("SET search_path TO public")
21
+
22
+ # Load models after connection is established
23
+ require_models
24
+
25
+ true
26
+ end
27
+
28
+ # Load and parse database configuration from YAML with ERB
29
+ def load_database_config
30
+ config_path = File.expand_path('../../config/database.yml', __dir__)
31
+
32
+ unless File.exist?(config_path)
33
+ raise "Database configuration file not found at #{config_path}"
34
+ end
35
+
36
+ # Read and parse ERB
37
+ erb_content = ERB.new(File.read(config_path)).result
38
+ db_config = YAML.safe_load(erb_content, aliases: true)
39
+
40
+ # Determine environment
41
+ env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
42
+
43
+ # Get configuration for current environment
44
+ config = db_config[env]
45
+
46
+ unless config
47
+ raise "No database configuration found for environment: #{env}"
48
+ end
49
+
50
+ # Convert string keys to symbols for ActiveRecord
51
+ config.transform_keys(&:to_sym)
52
+ end
53
+
54
+ # Check if connection is established and active
55
+ def connected?
56
+ ActiveRecord::Base.connected? &&
57
+ ActiveRecord::Base.connection.active?
58
+ rescue StandardError
59
+ false
60
+ end
61
+
62
+ # Close all database connections
63
+ def disconnect!
64
+ ActiveRecord::Base.connection_pool.disconnect!
65
+ end
66
+
67
+ # Verify required extensions are available
68
+ def verify_extensions!
69
+ conn = ActiveRecord::Base.connection
70
+
71
+ required_extensions = {
72
+ 'vector' => 'pgvector extension',
73
+ 'pg_trgm' => 'PostgreSQL trigram extension'
74
+ }
75
+
76
+ missing = []
77
+ required_extensions.each do |ext, name|
78
+ result = conn.select_value(
79
+ "SELECT COUNT(*) FROM pg_extension WHERE extname = '#{ext}'"
80
+ )
81
+ missing << name if result.to_i.zero?
82
+ end
83
+
84
+ if missing.any?
85
+ raise "Missing required PostgreSQL extensions: #{missing.join(', ')}"
86
+ end
87
+
88
+ true
89
+ end
90
+
91
+ # Get connection pool statistics
92
+ def connection_stats
93
+ pool = ActiveRecord::Base.connection_pool
94
+ {
95
+ size: pool.size,
96
+ connections: pool.connections.size,
97
+ in_use: pool.connections.count(&:in_use?),
98
+ available: pool.connections.count { |c| !c.in_use? }
99
+ }
100
+ end
101
+
102
+ private
103
+
104
+ # Require all model files
105
+ def require_models
106
+ require_relative 'models/robot'
107
+ require_relative 'models/node'
108
+ require_relative 'models/tag'
109
+ require_relative 'models/node_tag'
110
+ end
111
+ end
112
+ end
113
+ end