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.
- checksums.yaml +7 -0
- data/.architecture/decisions/adrs/001-use-postgresql-timescaledb-storage.md +227 -0
- data/.architecture/decisions/adrs/002-two-tier-memory-architecture.md +322 -0
- data/.architecture/decisions/adrs/003-ollama-default-embedding-provider.md +339 -0
- data/.architecture/decisions/adrs/004-multi-robot-shared-memory-hive-mind.md +374 -0
- data/.architecture/decisions/adrs/005-rag-based-retrieval-with-hybrid-search.md +443 -0
- data/.architecture/decisions/adrs/006-context-assembly-strategies.md +444 -0
- data/.architecture/decisions/adrs/007-working-memory-eviction-strategy.md +461 -0
- data/.architecture/decisions/adrs/008-robot-identification-system.md +550 -0
- data/.architecture/decisions/adrs/009-never-forget-explicit-deletion-only.md +570 -0
- data/.architecture/decisions/adrs/010-redis-working-memory-rejected.md +323 -0
- data/.architecture/decisions/adrs/011-database-side-embedding-generation-with-pgai.md +585 -0
- data/.architecture/decisions/adrs/012-llm-driven-ontology-topic-extraction.md +583 -0
- data/.architecture/decisions/adrs/013-activerecord-orm-and-many-to-many-tagging.md +299 -0
- data/.architecture/decisions/adrs/014-client-side-embedding-generation-workflow.md +569 -0
- data/.architecture/decisions/adrs/015-hierarchical-tag-ontology-and-llm-extraction.md +701 -0
- data/.architecture/decisions/adrs/016-async-embedding-and-tag-generation.md +694 -0
- data/.architecture/members.yml +144 -0
- data/.architecture/reviews/2025-10-29-llm-configuration-and-async-processing-review.md +1137 -0
- data/.architecture/reviews/initial-system-analysis.md +330 -0
- data/.envrc +32 -0
- data/.irbrc +145 -0
- data/CHANGELOG.md +150 -0
- data/COMMITS.md +196 -0
- data/LICENSE +21 -0
- data/README.md +1347 -0
- data/Rakefile +51 -0
- data/SETUP.md +268 -0
- data/config/database.yml +67 -0
- data/db/migrate/20250101000001_enable_extensions.rb +14 -0
- data/db/migrate/20250101000002_create_robots.rb +14 -0
- data/db/migrate/20250101000003_create_nodes.rb +42 -0
- data/db/migrate/20250101000005_create_tags.rb +38 -0
- data/db/migrate/20250101000007_add_node_vector_indexes.rb +30 -0
- data/db/schema.sql +473 -0
- data/db/seed_data/README.md +100 -0
- data/db/seed_data/presidents.md +136 -0
- data/db/seed_data/states.md +151 -0
- data/db/seeds.rb +208 -0
- data/dbdoc/README.md +173 -0
- data/dbdoc/public.node_stats.md +48 -0
- data/dbdoc/public.node_stats.svg +41 -0
- data/dbdoc/public.node_tags.md +40 -0
- data/dbdoc/public.node_tags.svg +112 -0
- data/dbdoc/public.nodes.md +54 -0
- data/dbdoc/public.nodes.svg +118 -0
- data/dbdoc/public.nodes_tags.md +39 -0
- data/dbdoc/public.nodes_tags.svg +112 -0
- data/dbdoc/public.ontology_structure.md +48 -0
- data/dbdoc/public.ontology_structure.svg +38 -0
- data/dbdoc/public.operations_log.md +42 -0
- data/dbdoc/public.operations_log.svg +130 -0
- data/dbdoc/public.relationships.md +39 -0
- data/dbdoc/public.relationships.svg +41 -0
- data/dbdoc/public.robot_activity.md +46 -0
- data/dbdoc/public.robot_activity.svg +35 -0
- data/dbdoc/public.robots.md +35 -0
- data/dbdoc/public.robots.svg +90 -0
- data/dbdoc/public.schema_migrations.md +29 -0
- data/dbdoc/public.schema_migrations.svg +26 -0
- data/dbdoc/public.tags.md +35 -0
- data/dbdoc/public.tags.svg +60 -0
- data/dbdoc/public.topic_relationships.md +45 -0
- data/dbdoc/public.topic_relationships.svg +32 -0
- data/dbdoc/schema.json +1437 -0
- data/dbdoc/schema.svg +154 -0
- data/docs/api/database.md +806 -0
- data/docs/api/embedding-service.md +532 -0
- data/docs/api/htm.md +797 -0
- data/docs/api/index.md +259 -0
- data/docs/api/long-term-memory.md +1096 -0
- data/docs/api/working-memory.md +665 -0
- data/docs/architecture/adrs/001-postgresql-timescaledb.md +314 -0
- data/docs/architecture/adrs/002-two-tier-memory.md +411 -0
- data/docs/architecture/adrs/003-ollama-embeddings.md +421 -0
- data/docs/architecture/adrs/004-hive-mind.md +437 -0
- data/docs/architecture/adrs/005-rag-retrieval.md +531 -0
- data/docs/architecture/adrs/006-context-assembly.md +496 -0
- data/docs/architecture/adrs/007-eviction-strategy.md +645 -0
- data/docs/architecture/adrs/008-robot-identification.md +625 -0
- data/docs/architecture/adrs/009-never-forget.md +648 -0
- data/docs/architecture/adrs/010-redis-working-memory-rejected.md +323 -0
- data/docs/architecture/adrs/011-pgai-integration.md +494 -0
- data/docs/architecture/adrs/index.md +215 -0
- data/docs/architecture/hive-mind.md +736 -0
- data/docs/architecture/index.md +351 -0
- data/docs/architecture/overview.md +538 -0
- data/docs/architecture/two-tier-memory.md +873 -0
- data/docs/assets/css/custom.css +83 -0
- data/docs/assets/images/htm-core-components.svg +63 -0
- data/docs/assets/images/htm-database-schema.svg +93 -0
- data/docs/assets/images/htm-hive-mind-architecture.svg +125 -0
- data/docs/assets/images/htm-importance-scoring-framework.svg +83 -0
- data/docs/assets/images/htm-layered-architecture.svg +71 -0
- data/docs/assets/images/htm-long-term-memory-architecture.svg +115 -0
- data/docs/assets/images/htm-working-memory-architecture.svg +120 -0
- data/docs/assets/images/htm.jpg +0 -0
- data/docs/assets/images/htm_demo.gif +0 -0
- data/docs/assets/js/mathjax.js +18 -0
- data/docs/assets/videos/htm_video.mp4 +0 -0
- data/docs/database_rake_tasks.md +322 -0
- data/docs/development/contributing.md +787 -0
- data/docs/development/index.md +336 -0
- data/docs/development/schema.md +596 -0
- data/docs/development/setup.md +719 -0
- data/docs/development/testing.md +819 -0
- data/docs/guides/adding-memories.md +824 -0
- data/docs/guides/context-assembly.md +1009 -0
- data/docs/guides/getting-started.md +577 -0
- data/docs/guides/index.md +118 -0
- data/docs/guides/long-term-memory.md +941 -0
- data/docs/guides/multi-robot.md +866 -0
- data/docs/guides/recalling-memories.md +927 -0
- data/docs/guides/search-strategies.md +953 -0
- data/docs/guides/working-memory.md +717 -0
- data/docs/index.md +214 -0
- data/docs/installation.md +477 -0
- data/docs/multi_framework_support.md +519 -0
- data/docs/quick-start.md +655 -0
- data/docs/setup_local_database.md +302 -0
- data/docs/using_rake_tasks_in_your_app.md +383 -0
- data/examples/basic_usage.rb +93 -0
- data/examples/cli_app/README.md +317 -0
- data/examples/cli_app/htm_cli.rb +270 -0
- data/examples/custom_llm_configuration.rb +183 -0
- data/examples/example_app/Rakefile +71 -0
- data/examples/example_app/app.rb +206 -0
- data/examples/sinatra_app/Gemfile +21 -0
- data/examples/sinatra_app/app.rb +335 -0
- data/lib/htm/active_record_config.rb +113 -0
- data/lib/htm/configuration.rb +342 -0
- data/lib/htm/database.rb +594 -0
- data/lib/htm/embedding_service.rb +115 -0
- data/lib/htm/errors.rb +34 -0
- data/lib/htm/job_adapter.rb +154 -0
- data/lib/htm/jobs/generate_embedding_job.rb +65 -0
- data/lib/htm/jobs/generate_tags_job.rb +82 -0
- data/lib/htm/long_term_memory.rb +965 -0
- data/lib/htm/models/node.rb +109 -0
- data/lib/htm/models/node_tag.rb +33 -0
- data/lib/htm/models/robot.rb +52 -0
- data/lib/htm/models/tag.rb +76 -0
- data/lib/htm/railtie.rb +76 -0
- data/lib/htm/sinatra.rb +157 -0
- data/lib/htm/tag_service.rb +135 -0
- data/lib/htm/tasks.rb +38 -0
- data/lib/htm/version.rb +5 -0
- data/lib/htm/working_memory.rb +182 -0
- data/lib/htm.rb +400 -0
- data/lib/tasks/db.rake +19 -0
- data/lib/tasks/htm.rake +147 -0
- data/lib/tasks/jobs.rake +312 -0
- data/mkdocs.yml +190 -0
- data/scripts/install_local_database.sh +309 -0
- 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
|