htm 0.0.31 → 0.0.32
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/.irbrc +2 -3
- data/.rubocop.yml +184 -0
- data/CHANGELOG.md +46 -0
- data/README.md +2 -0
- data/Rakefile +93 -12
- data/db/migrate/00008_create_node_relationships.rb +54 -0
- data/db/migrate/00009_fix_node_relationships_column_types.rb +17 -0
- data/db/schema.sql +124 -1
- data/docs/api/database.md +35 -57
- data/docs/api/embedding-service.md +1 -1
- data/docs/api/index.md +26 -15
- data/docs/api/working-memory.md +8 -8
- data/docs/architecture/index.md +5 -7
- data/docs/architecture/overview.md +5 -8
- data/docs/assets/images/htm-architecture-overview.svg +1 -1
- data/docs/assets/images/htm-context-assembly-flow.svg +2 -2
- data/docs/assets/images/htm-layered-architecture.svg +3 -3
- data/docs/assets/images/two-tier-memory-architecture.svg +1 -1
- data/docs/database/README.md +1 -0
- data/docs/database_rake_tasks.md +20 -28
- data/docs/development/contributing.md +5 -5
- data/docs/development/index.md +4 -7
- data/docs/development/schema.md +71 -1
- data/docs/development/setup.md +40 -82
- data/docs/development/testing.md +1 -1
- data/docs/examples/file-loading.md +4 -4
- data/docs/examples/mcp-client.md +1 -1
- data/docs/getting-started/quick-start.md +4 -4
- data/docs/guides/adding-memories.md +14 -1
- data/docs/guides/configuration.md +5 -5
- data/docs/guides/context-assembly.md +4 -4
- data/docs/guides/file-loading.md +12 -12
- data/docs/guides/getting-started.md +2 -2
- data/docs/guides/long-term-memory.md +7 -27
- data/docs/guides/propositions.md +20 -19
- data/docs/guides/recalling-memories.md +5 -5
- data/docs/guides/tags.md +18 -13
- data/docs/multi_framework_support.md +1 -1
- data/docs/robots/hive-mind.md +1 -1
- data/docs/robots/multi-robot.md +2 -2
- data/docs/robots/robot-groups.md +1 -1
- data/docs/robots/two-tier-memory.md +72 -94
- data/docs/setup_local_database.md +8 -54
- data/docs/using_rake_tasks_in_your_app.md +6 -6
- data/examples/01_basic_usage.rb +1 -0
- data/examples/03_custom_llm_configuration.rb +1 -0
- data/examples/04_file_loader_usage.rb +1 -0
- data/examples/05_timeframe_demo.rb +1 -0
- data/examples/06_example_app/app.rb +1 -0
- data/examples/07_cli_app/htm_cli.rb +1 -0
- data/examples/09_mcp_client.rb +1 -0
- data/examples/10_telemetry/demo.rb +1 -0
- data/examples/11_robot_groups/multi_process.rb +1 -0
- data/examples/11_robot_groups/same_process.rb +1 -0
- data/examples/12_rails_app/.envrc +12 -0
- data/examples/12_rails_app/Gemfile +8 -3
- data/examples/12_rails_app/Gemfile.lock +94 -89
- data/examples/12_rails_app/README.md +70 -19
- data/examples/12_rails_app/app/controllers/application_controller.rb +6 -0
- data/examples/12_rails_app/app/controllers/chats_controller.rb +305 -0
- data/examples/12_rails_app/app/controllers/dashboard_controller.rb +3 -0
- data/examples/12_rails_app/app/controllers/files_controller.rb +17 -2
- data/examples/12_rails_app/app/controllers/home_controller.rb +8 -0
- data/examples/12_rails_app/app/controllers/memories_controller.rb +9 -4
- data/examples/12_rails_app/app/controllers/messages_controller.rb +214 -0
- data/examples/12_rails_app/app/controllers/robots_controller.rb +11 -1
- data/examples/12_rails_app/app/controllers/tags_controller.rb +14 -1
- data/examples/12_rails_app/app/javascript/application.js +1 -1
- data/examples/12_rails_app/app/models/application_record.rb +5 -0
- data/examples/12_rails_app/app/models/chat.rb +36 -0
- data/examples/12_rails_app/app/models/message.rb +5 -0
- data/examples/12_rails_app/app/models/model.rb +5 -0
- data/examples/12_rails_app/app/models/tool_call.rb +5 -0
- data/examples/12_rails_app/app/views/chats/index.html.erb +61 -0
- data/examples/12_rails_app/app/views/chats/show.html.erb +213 -0
- data/examples/12_rails_app/app/views/dashboard/index.html.erb +3 -0
- data/examples/12_rails_app/app/views/files/index.html.erb +10 -5
- data/examples/12_rails_app/app/views/files/new.html.erb +4 -2
- data/examples/12_rails_app/app/views/files/show.html.erb +19 -3
- data/examples/12_rails_app/app/views/home/index.html.erb +45 -0
- data/examples/12_rails_app/app/views/layouts/application.html.erb +20 -18
- data/examples/12_rails_app/app/views/memories/_memory_card.html.erb +1 -1
- data/examples/12_rails_app/app/views/memories/deleted.html.erb +3 -1
- data/examples/12_rails_app/app/views/memories/edit.html.erb +2 -0
- data/examples/12_rails_app/app/views/memories/index.html.erb +2 -0
- data/examples/12_rails_app/app/views/memories/new.html.erb +2 -0
- data/examples/12_rails_app/app/views/memories/show.html.erb +4 -2
- data/examples/12_rails_app/app/views/messages/_message.html.erb +20 -0
- data/examples/12_rails_app/app/views/robots/index.html.erb +2 -0
- data/examples/12_rails_app/app/views/robots/new.html.erb +2 -0
- data/examples/12_rails_app/app/views/robots/show.html.erb +2 -0
- data/examples/12_rails_app/app/views/search/index.html.erb +59 -8
- data/examples/12_rails_app/app/views/shared/_navbar.html.erb +75 -29
- data/examples/12_rails_app/app/views/tags/index.html.erb +2 -0
- data/examples/12_rails_app/app/views/tags/show.html.erb +3 -1
- data/examples/12_rails_app/config/application.rb +1 -1
- data/examples/12_rails_app/config/database.yml +9 -5
- data/examples/12_rails_app/config/importmap.rb +1 -1
- data/examples/12_rails_app/config/initializers/htm.rb +9 -2
- data/examples/12_rails_app/config/initializers/ruby_llm.rb +33 -0
- data/examples/12_rails_app/config/routes.rb +39 -23
- data/examples/12_rails_app/db/migrate/20250124000001_create_ruby_llm_tables.rb +34 -0
- data/examples/12_rails_app/db/migrate/20250124000002_create_models_table.rb +28 -0
- data/examples/12_rails_app/db/schema.rb +67 -0
- data/examples/examples_helper.rb +25 -0
- data/lib/htm/circuit_breaker.rb +5 -6
- data/lib/htm/config/builder.rb +12 -12
- data/lib/htm/config/database.rb +21 -27
- data/lib/htm/config/validator.rb +12 -18
- data/lib/htm/config.rb +76 -65
- data/lib/htm/database.rb +193 -199
- data/lib/htm/embedding_service.rb +4 -9
- data/lib/htm/integrations/sinatra.rb +7 -7
- data/lib/htm/job_adapter.rb +14 -21
- data/lib/htm/jobs/generate_embedding_job.rb +28 -44
- data/lib/htm/jobs/generate_propositions_job.rb +29 -55
- data/lib/htm/jobs/generate_relationships_job.rb +137 -0
- data/lib/htm/jobs/generate_tags_job.rb +45 -67
- data/lib/htm/loaders/markdown_loader.rb +65 -112
- data/lib/htm/long_term_memory/fulltext_search.rb +1 -1
- data/lib/htm/long_term_memory/hybrid_search.rb +300 -128
- data/lib/htm/long_term_memory/node_operations.rb +2 -2
- data/lib/htm/long_term_memory/relevance_scorer.rb +100 -68
- data/lib/htm/long_term_memory/tag_operations.rb +87 -120
- data/lib/htm/long_term_memory/vector_search.rb +1 -1
- data/lib/htm/long_term_memory.rb +2 -1
- data/lib/htm/mcp/cli.rb +59 -58
- data/lib/htm/mcp/server.rb +5 -6
- data/lib/htm/mcp/tools.rb +30 -36
- data/lib/htm/migration.rb +10 -10
- data/lib/htm/models/node.rb +2 -3
- data/lib/htm/models/node_relationship.rb +72 -0
- data/lib/htm/models/node_tag.rb +2 -2
- data/lib/htm/models/robot_node.rb +2 -2
- data/lib/htm/models/tag.rb +41 -28
- data/lib/htm/observability.rb +45 -51
- data/lib/htm/proposition_service.rb +3 -7
- data/lib/htm/query_cache.rb +13 -15
- data/lib/htm/railtie.rb +1 -2
- data/lib/htm/robot_group.rb +9 -9
- data/lib/htm/sequel_config.rb +1 -0
- data/lib/htm/sql_builder.rb +1 -1
- data/lib/htm/tag_service.rb +2 -6
- data/lib/htm/timeframe.rb +4 -5
- data/lib/htm/timeframe_extractor.rb +42 -83
- data/lib/htm/version.rb +1 -1
- data/lib/htm/workflows/remember_workflow.rb +112 -115
- data/lib/htm/working_memory.rb +21 -26
- data/lib/htm.rb +103 -116
- data/lib/tasks/db.rake +0 -2
- data/lib/tasks/doc.rake +14 -13
- data/lib/tasks/files.rake +5 -12
- data/lib/tasks/htm.rake +70 -71
- data/lib/tasks/jobs.rake +41 -47
- data/lib/tasks/tags.rake +3 -8
- metadata +25 -100
data/lib/htm/database.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'pg'
|
|
4
4
|
require 'uri'
|
|
5
|
-
require 'set'
|
|
6
5
|
|
|
7
6
|
class HTM
|
|
8
7
|
# Database setup and configuration for HTM
|
|
@@ -35,10 +34,9 @@ class HTM
|
|
|
35
34
|
puts "HTM database schema created successfully"
|
|
36
35
|
|
|
37
36
|
# Optionally dump schema
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
end
|
|
37
|
+
return unless dump_schema
|
|
38
|
+
puts ""
|
|
39
|
+
self.dump_schema(db_url)
|
|
42
40
|
end
|
|
43
41
|
|
|
44
42
|
# Run pending database migrations
|
|
@@ -70,45 +68,12 @@ class HTM
|
|
|
70
68
|
require 'sequel'
|
|
71
69
|
require_relative 'sequel_config'
|
|
72
70
|
|
|
73
|
-
# Establish Sequel connection (don't load models - we just need the DB)
|
|
74
71
|
HTM::SequelConfig.establish_connection!(load_models: false)
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
# Get available migrations from files
|
|
79
|
-
available_migrations = Dir.glob(File.join(migrations_path, '*.rb')).map do |file|
|
|
80
|
-
{
|
|
81
|
-
version: File.basename(file).split('_').first.to_i,
|
|
82
|
-
name: File.basename(file, '.rb')
|
|
83
|
-
}
|
|
84
|
-
end.sort_by { |m| m[:version] }
|
|
85
|
-
|
|
86
|
-
# Get applied migrations from database
|
|
87
|
-
applied_versions = begin
|
|
88
|
-
HTM.db[:schema_migrations].select_map(:version).map(&:to_i)
|
|
89
|
-
rescue Sequel::DatabaseError
|
|
90
|
-
[]
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
puts "\nMigration Status"
|
|
94
|
-
puts "=" * 100
|
|
95
|
-
|
|
96
|
-
if available_migrations.empty?
|
|
97
|
-
puts "No migration files found in db/migrate/"
|
|
98
|
-
else
|
|
99
|
-
available_migrations.each do |migration|
|
|
100
|
-
status = applied_versions.include?(migration[:version])
|
|
101
|
-
status_mark = status ? "[x]" : "[ ]"
|
|
102
|
-
|
|
103
|
-
puts "#{status_mark} #{migration[:name]}"
|
|
104
|
-
end
|
|
105
|
-
end
|
|
73
|
+
available = load_available_migrations
|
|
74
|
+
applied = load_applied_versions
|
|
106
75
|
|
|
107
|
-
|
|
108
|
-
pending_count = available_migrations.length - applied_count
|
|
109
|
-
|
|
110
|
-
puts "\nSummary: #{applied_count} applied, #{pending_count} pending"
|
|
111
|
-
puts "=" * 100
|
|
76
|
+
print_migration_status_table(available, applied)
|
|
112
77
|
end
|
|
113
78
|
|
|
114
79
|
# Drop all HTM tables (respects RAILS_ENV)
|
|
@@ -125,16 +90,14 @@ class HTM
|
|
|
125
90
|
|
|
126
91
|
conn = PG.connect(config)
|
|
127
92
|
|
|
128
|
-
tables = [
|
|
93
|
+
tables = %w[nodes node_tags tags robots robot_nodes file_sources schema_migrations]
|
|
129
94
|
|
|
130
95
|
puts "Dropping HTM tables..."
|
|
131
96
|
tables.each do |table|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
puts " Error dropping #{table}: #{e.message}"
|
|
137
|
-
end
|
|
97
|
+
conn.exec("DROP TABLE IF EXISTS #{table} CASCADE")
|
|
98
|
+
puts " Dropped #{table}"
|
|
99
|
+
rescue PG::Error => e
|
|
100
|
+
puts " Error dropping #{table}: #{e.message}"
|
|
138
101
|
end
|
|
139
102
|
|
|
140
103
|
# Drop functions and triggers
|
|
@@ -264,7 +227,7 @@ class HTM
|
|
|
264
227
|
]
|
|
265
228
|
|
|
266
229
|
require 'open3'
|
|
267
|
-
|
|
230
|
+
_, stderr, status = Open3.capture3(env, *cmd)
|
|
268
231
|
|
|
269
232
|
unless status.success?
|
|
270
233
|
puts "Error loading schema:"
|
|
@@ -281,60 +244,11 @@ class HTM
|
|
|
281
244
|
# @return [void]
|
|
282
245
|
#
|
|
283
246
|
def generate_docs(db_url = nil)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
puts " # or"
|
|
290
|
-
puts " go install github.com/k1LoW/tbls@latest"
|
|
291
|
-
puts ""
|
|
292
|
-
puts "See: https://github.com/k1LoW/tbls"
|
|
293
|
-
exit 1
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
project_root = File.expand_path('../..', __dir__)
|
|
297
|
-
tbls_config = File.join(project_root, '.tbls.yml')
|
|
298
|
-
|
|
299
|
-
unless File.exist?(tbls_config)
|
|
300
|
-
puts "Error: .tbls.yml not found at #{tbls_config}"
|
|
301
|
-
exit 1
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
dsn = db_url || ENV['HTM_DATABASE__URL']
|
|
305
|
-
raise "Database configuration not found. Set HTM_DATABASE__URL environment variable." unless dsn
|
|
306
|
-
|
|
307
|
-
unless dsn.include?('sslmode=')
|
|
308
|
-
separator = dsn.include?('?') ? '&' : '?'
|
|
309
|
-
dsn = "#{dsn}#{separator}sslmode=disable"
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
puts "Generating database documentation using #{tbls_config}..."
|
|
313
|
-
|
|
314
|
-
require 'open3'
|
|
315
|
-
cmd = ['tbls', 'doc', '--config', tbls_config, '--dsn', dsn, '--force']
|
|
316
|
-
|
|
317
|
-
stdout, stderr, status = Open3.capture3(*cmd)
|
|
318
|
-
|
|
319
|
-
unless status.success?
|
|
320
|
-
puts "Error generating documentation:"
|
|
321
|
-
puts stderr
|
|
322
|
-
puts stdout
|
|
323
|
-
exit 1
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
puts stdout if stdout && !stdout.empty?
|
|
327
|
-
|
|
328
|
-
doc_path = 'docs/database'
|
|
329
|
-
puts "Database documentation generated successfully"
|
|
330
|
-
puts ""
|
|
331
|
-
puts "Documentation files:"
|
|
332
|
-
puts " #{doc_path}/README.md - Main documentation"
|
|
333
|
-
puts " #{doc_path}/schema.svg - ER diagram"
|
|
334
|
-
puts " #{doc_path}/*.md - Individual table documentation"
|
|
335
|
-
puts ""
|
|
336
|
-
puts "View documentation:"
|
|
337
|
-
puts " open #{doc_path}/README.md"
|
|
247
|
+
check_tbls_installed!
|
|
248
|
+
tbls_config = locate_tbls_config!
|
|
249
|
+
dsn = build_tbls_dsn(db_url)
|
|
250
|
+
run_tbls_doc(tbls_config, dsn)
|
|
251
|
+
print_tbls_doc_success
|
|
338
252
|
end
|
|
339
253
|
|
|
340
254
|
# Show database info (respects RAILS_ENV)
|
|
@@ -347,44 +261,13 @@ class HTM
|
|
|
347
261
|
raise "Database configuration not found" unless config
|
|
348
262
|
|
|
349
263
|
conn = PG.connect(config)
|
|
350
|
-
|
|
351
264
|
puts "\nHTM Database Information (#{HTM.env})"
|
|
352
265
|
puts "=" * 80
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
puts " Database: #{config[:dbname]}"
|
|
359
|
-
puts " User: #{config[:user]}"
|
|
360
|
-
|
|
361
|
-
version = conn.exec("SELECT version()").first['version']
|
|
362
|
-
puts "\nPostgreSQL Version:"
|
|
363
|
-
puts " #{version.split(',').first}"
|
|
364
|
-
|
|
365
|
-
puts "\nExtensions:"
|
|
366
|
-
extensions = conn.exec("SELECT extname, extversion FROM pg_extension ORDER BY extname").to_a
|
|
367
|
-
extensions.each do |ext|
|
|
368
|
-
puts " #{ext['extname']} (#{ext['extversion']})"
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
puts "\nHTM Tables:"
|
|
372
|
-
tables = ['nodes', 'node_tags', 'tags', 'robots', 'robot_nodes', 'file_sources', 'schema_migrations']
|
|
373
|
-
tables.each do |table|
|
|
374
|
-
begin
|
|
375
|
-
count = conn.exec("SELECT COUNT(*) FROM #{table}").first['count']
|
|
376
|
-
puts " #{table}: #{count} rows"
|
|
377
|
-
rescue PG::UndefinedTable
|
|
378
|
-
puts " #{table}: not created"
|
|
379
|
-
end
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
db_size = conn.exec(
|
|
383
|
-
"SELECT pg_size_pretty(pg_database_size($1)) AS size",
|
|
384
|
-
[config[:dbname]]
|
|
385
|
-
).first['size']
|
|
386
|
-
puts "\nDatabase Size: #{db_size}"
|
|
387
|
-
|
|
266
|
+
print_connection_info(config)
|
|
267
|
+
print_pg_version(conn)
|
|
268
|
+
print_extensions_list(conn)
|
|
269
|
+
print_table_counts(conn)
|
|
270
|
+
print_db_size(conn, config[:dbname])
|
|
388
271
|
conn.close
|
|
389
272
|
puts "=" * 80
|
|
390
273
|
end
|
|
@@ -399,20 +282,9 @@ class HTM
|
|
|
399
282
|
return nil unless url
|
|
400
283
|
|
|
401
284
|
uri = URI.parse(url)
|
|
402
|
-
|
|
403
|
-
unless uri.scheme&.match?(/\Apostgres(?:ql)?\z/i)
|
|
404
|
-
raise ArgumentError, "Invalid database URL scheme: #{uri.scheme}. Expected 'postgresql' or 'postgres'."
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
unless uri.host && !uri.host.empty?
|
|
408
|
-
raise ArgumentError, "Database URL must include a host"
|
|
409
|
-
end
|
|
285
|
+
validate_pg_uri!(uri)
|
|
410
286
|
|
|
411
287
|
dbname = uri.path&.slice(1..-1)
|
|
412
|
-
if dbname.nil? || dbname.empty?
|
|
413
|
-
raise ArgumentError, "Database URL must include a database name (path segment)"
|
|
414
|
-
end
|
|
415
|
-
|
|
416
288
|
params = URI.decode_www_form(uri.query || '').to_h
|
|
417
289
|
|
|
418
290
|
{
|
|
@@ -437,9 +309,9 @@ class HTM
|
|
|
437
309
|
{
|
|
438
310
|
host: ENV['HTM_DATABASE__HOST'] || 'localhost',
|
|
439
311
|
port: (ENV['HTM_DATABASE__PORT'] || 5432).to_i,
|
|
440
|
-
dbname: ENV
|
|
441
|
-
user: ENV
|
|
442
|
-
password: ENV
|
|
312
|
+
dbname: ENV.fetch('HTM_DATABASE__NAME', nil),
|
|
313
|
+
user: ENV.fetch('HTM_DATABASE__USER', nil),
|
|
314
|
+
password: ENV.fetch('HTM_DATABASE__PASSWORD', nil),
|
|
443
315
|
sslmode: ENV['HTM_DATABASE__SSLMODE'] || 'prefer'
|
|
444
316
|
}
|
|
445
317
|
end
|
|
@@ -474,6 +346,65 @@ class HTM
|
|
|
474
346
|
|
|
475
347
|
private
|
|
476
348
|
|
|
349
|
+
def print_connection_info(config)
|
|
350
|
+
puts "\nConnection:"
|
|
351
|
+
puts " Environment: #{HTM.env}"
|
|
352
|
+
puts " Host: #{config[:host]}"
|
|
353
|
+
puts " Port: #{config[:port]}"
|
|
354
|
+
puts " Database: #{config[:dbname]}"
|
|
355
|
+
puts " User: #{config[:user]}"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def print_pg_version(conn)
|
|
359
|
+
version = conn.exec("SELECT version()").first['version']
|
|
360
|
+
puts "\nPostgreSQL Version:"
|
|
361
|
+
puts " #{version.split(',').first}"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def print_extensions_list(conn)
|
|
365
|
+
puts "\nExtensions:"
|
|
366
|
+
conn.exec("SELECT extname, extversion FROM pg_extension ORDER BY extname").each do |ext|
|
|
367
|
+
puts " #{ext['extname']} (#{ext['extversion']})"
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def print_table_counts(conn)
|
|
372
|
+
puts "\nHTM Tables:"
|
|
373
|
+
%w[nodes node_tags tags robots robot_nodes file_sources schema_migrations].each do |table|
|
|
374
|
+
count = conn.exec("SELECT COUNT(*) FROM #{table}").first['count']
|
|
375
|
+
puts " #{table}: #{count} rows"
|
|
376
|
+
rescue PG::UndefinedTable
|
|
377
|
+
puts " #{table}: not created"
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def print_db_size(conn, dbname)
|
|
382
|
+
size = conn.exec("SELECT pg_size_pretty(pg_database_size($1)) AS size", [dbname]).first['size']
|
|
383
|
+
puts "\nDatabase Size: #{size}"
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def ensure_schema_migrations_table(db)
|
|
387
|
+
return if db.table_exists?(:schema_migrations)
|
|
388
|
+
db.create_table(:schema_migrations) { String :version, primary_key: true, null: false }
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def run_migration_file(file, db)
|
|
392
|
+
version = File.basename(file).split('_').first
|
|
393
|
+
name = File.basename(file, '.rb')
|
|
394
|
+
|
|
395
|
+
if db[:schema_migrations].where(version: version).any?
|
|
396
|
+
puts " [x] #{name} (already migrated)"
|
|
397
|
+
else
|
|
398
|
+
puts " --> Running #{name}..."
|
|
399
|
+
require file
|
|
400
|
+
class_name = name.split('_')[1..].map(&:capitalize).join
|
|
401
|
+
migration_class = Object.const_get(class_name)
|
|
402
|
+
migration_class.new(db).up
|
|
403
|
+
db[:schema_migrations].insert(version: version)
|
|
404
|
+
puts " Completed"
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
477
408
|
def verify_extensions(conn)
|
|
478
409
|
# Check pgvector
|
|
479
410
|
pgvector = conn.exec("SELECT extversion FROM pg_extension WHERE extname='vector'").first
|
|
@@ -506,54 +437,17 @@ class HTM
|
|
|
506
437
|
#
|
|
507
438
|
def run_sequel_migrations
|
|
508
439
|
migrations_path = File.expand_path('../../db/migrate', __dir__)
|
|
509
|
-
|
|
510
440
|
unless Dir.exist?(migrations_path)
|
|
511
441
|
puts "No migrations directory found at #{migrations_path}"
|
|
512
442
|
return
|
|
513
443
|
end
|
|
514
444
|
|
|
515
445
|
db = HTM.db
|
|
446
|
+
ensure_schema_migrations_table(db)
|
|
516
447
|
|
|
517
|
-
|
|
518
|
-
unless db.table_exists?(:schema_migrations)
|
|
519
|
-
db.create_table(:schema_migrations) do
|
|
520
|
-
String :version, primary_key: true, null: false
|
|
521
|
-
end
|
|
522
|
-
end
|
|
523
|
-
|
|
524
|
-
# Get list of migration files
|
|
525
|
-
migration_files = Dir.glob("#{migrations_path}/*.rb").sort
|
|
448
|
+
migration_files = Dir.glob("#{migrations_path}/*.rb")
|
|
526
449
|
puts "Found #{migration_files.length} migration files"
|
|
527
|
-
|
|
528
|
-
# Run each migration
|
|
529
|
-
migration_files.each do |file|
|
|
530
|
-
version = File.basename(file).split('_').first
|
|
531
|
-
name = File.basename(file, '.rb')
|
|
532
|
-
|
|
533
|
-
# Check if already run
|
|
534
|
-
already_run = db[:schema_migrations].where(version: version).count > 0
|
|
535
|
-
|
|
536
|
-
if already_run
|
|
537
|
-
puts " [x] #{name} (already migrated)"
|
|
538
|
-
else
|
|
539
|
-
puts " --> Running #{name}..."
|
|
540
|
-
require file
|
|
541
|
-
|
|
542
|
-
# Get the migration class
|
|
543
|
-
class_name = name.split('_')[1..].map(&:capitalize).join
|
|
544
|
-
migration_class = Object.const_get(class_name)
|
|
545
|
-
|
|
546
|
-
# Run the migration
|
|
547
|
-
migration = migration_class.new(db)
|
|
548
|
-
migration.up
|
|
549
|
-
|
|
550
|
-
# Record in schema_migrations
|
|
551
|
-
db[:schema_migrations].insert(version: version)
|
|
552
|
-
|
|
553
|
-
puts " Completed"
|
|
554
|
-
end
|
|
555
|
-
end
|
|
556
|
-
|
|
450
|
+
migration_files.each { |file| run_migration_file(file, db) }
|
|
557
451
|
puts "All migrations completed"
|
|
558
452
|
end
|
|
559
453
|
|
|
@@ -576,11 +470,9 @@ class HTM
|
|
|
576
470
|
|
|
577
471
|
lines.each do |line|
|
|
578
472
|
if skip_until_content
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
next
|
|
583
|
-
end
|
|
473
|
+
next unless line =~ /^(SET|CREATE|ALTER|--\s*Name:|COMMENT)/
|
|
474
|
+
skip_until_content = false
|
|
475
|
+
|
|
584
476
|
end
|
|
585
477
|
|
|
586
478
|
next if line =~ /^SET /
|
|
@@ -595,6 +487,108 @@ class HTM
|
|
|
595
487
|
|
|
596
488
|
result
|
|
597
489
|
end
|
|
490
|
+
|
|
491
|
+
def load_available_migrations
|
|
492
|
+
migrations_path = File.expand_path('../../db/migrate', __dir__)
|
|
493
|
+
migrations = Dir.glob(File.join(migrations_path, '*.rb')).map do |file|
|
|
494
|
+
{ version: File.basename(file).split('_').first.to_i, name: File.basename(file, '.rb') }
|
|
495
|
+
end
|
|
496
|
+
migrations.sort_by { |m| m[:version] }
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def load_applied_versions
|
|
500
|
+
HTM.db[:schema_migrations].select_map(:version).map(&:to_i)
|
|
501
|
+
rescue Sequel::DatabaseError
|
|
502
|
+
[]
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def print_migration_status_table(available, applied)
|
|
506
|
+
puts "\nMigration Status"
|
|
507
|
+
puts "=" * 100
|
|
508
|
+
if available.empty?
|
|
509
|
+
puts "No migration files found in db/migrate/"
|
|
510
|
+
else
|
|
511
|
+
available.each do |m|
|
|
512
|
+
mark = applied.include?(m[:version]) ? "[x]" : "[ ]"
|
|
513
|
+
puts "#{mark} #{m[:name]}"
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
pending = available.length - applied.length
|
|
517
|
+
puts "\nSummary: #{applied.length} applied, #{pending} pending"
|
|
518
|
+
puts "=" * 100
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def check_tbls_installed!
|
|
522
|
+
return if system('which tbls > /dev/null 2>&1')
|
|
523
|
+
|
|
524
|
+
puts <<~MSG
|
|
525
|
+
Error: 'tbls' is not installed
|
|
526
|
+
|
|
527
|
+
Install tbls:
|
|
528
|
+
brew install k1LoW/tap/tbls
|
|
529
|
+
# or
|
|
530
|
+
go install github.com/k1LoW/tbls@latest
|
|
531
|
+
|
|
532
|
+
See: https://github.com/k1LoW/tbls
|
|
533
|
+
MSG
|
|
534
|
+
exit 1
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def locate_tbls_config!
|
|
538
|
+
project_root = File.expand_path('../..', __dir__)
|
|
539
|
+
config_path = File.join(project_root, '.tbls.yml')
|
|
540
|
+
return config_path if File.exist?(config_path)
|
|
541
|
+
|
|
542
|
+
puts "Error: .tbls.yml not found at #{config_path}"
|
|
543
|
+
exit 1
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def build_tbls_dsn(db_url)
|
|
547
|
+
dsn = db_url || ENV.fetch('HTM_DATABASE__URL', nil)
|
|
548
|
+
raise "Database configuration not found. Set HTM_DATABASE__URL environment variable." unless dsn
|
|
549
|
+
|
|
550
|
+
return dsn if dsn.include?('sslmode=')
|
|
551
|
+
|
|
552
|
+
separator = dsn.include?('?') ? '&' : '?'
|
|
553
|
+
"#{dsn}#{separator}sslmode=disable"
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def run_tbls_doc(tbls_config, dsn)
|
|
557
|
+
require 'open3'
|
|
558
|
+
puts "Generating database documentation using #{tbls_config}..."
|
|
559
|
+
stdout, stderr, status = Open3.capture3('tbls', 'doc', '--config', tbls_config, '--dsn', dsn, '--force')
|
|
560
|
+
return puts(stdout) if status.success? && stdout && !stdout.empty?
|
|
561
|
+
|
|
562
|
+
puts "Error generating documentation:"
|
|
563
|
+
puts stderr
|
|
564
|
+
puts stdout
|
|
565
|
+
exit 1
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def print_tbls_doc_success
|
|
569
|
+
doc_path = 'docs/database'
|
|
570
|
+
puts <<~MSG
|
|
571
|
+
Database documentation generated successfully
|
|
572
|
+
|
|
573
|
+
Documentation files:
|
|
574
|
+
#{doc_path}/README.md - Main documentation
|
|
575
|
+
#{doc_path}/schema.svg - ER diagram
|
|
576
|
+
#{doc_path}/*.md - Individual table documentation
|
|
577
|
+
|
|
578
|
+
View documentation:
|
|
579
|
+
open #{doc_path}/README.md
|
|
580
|
+
MSG
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def validate_pg_uri!(uri)
|
|
584
|
+
unless uri.scheme&.match?(/\Apostgres(?:ql)?\z/i)
|
|
585
|
+
raise ArgumentError, "Invalid database URL scheme: #{uri.scheme}. Expected 'postgresql' or 'postgres'."
|
|
586
|
+
end
|
|
587
|
+
raise ArgumentError, "Database URL must include a host" if uri.host.nil? || uri.host.empty?
|
|
588
|
+
|
|
589
|
+
dbname = uri.path&.slice(1..-1)
|
|
590
|
+
raise ArgumentError, "Database URL must include a database name (path segment)" if dbname.nil? || dbname.empty?
|
|
591
|
+
end
|
|
598
592
|
end
|
|
599
593
|
end
|
|
600
594
|
end
|
|
@@ -99,11 +99,7 @@ class HTM
|
|
|
99
99
|
storage_embedding: storage_string,
|
|
100
100
|
storage_dimension: max_dim
|
|
101
101
|
}
|
|
102
|
-
|
|
103
|
-
rescue HTM::CircuitBreakerOpenError
|
|
104
|
-
# Re-raise circuit breaker errors without wrapping
|
|
105
|
-
raise
|
|
106
|
-
rescue HTM::EmbeddingError
|
|
102
|
+
rescue HTM::CircuitBreakerOpenError, HTM::EmbeddingError
|
|
107
103
|
raise
|
|
108
104
|
rescue StandardError => e
|
|
109
105
|
HTM.logger.error "EmbeddingService: Failed to generate embedding: #{e.message}"
|
|
@@ -124,14 +120,13 @@ class HTM
|
|
|
124
120
|
raise HTM::EmbeddingError, "Embedding array is empty"
|
|
125
121
|
end
|
|
126
122
|
|
|
127
|
-
unless embedding.all?
|
|
123
|
+
unless embedding.all?(Numeric)
|
|
128
124
|
raise HTM::EmbeddingError, "Embedding must contain only numeric values"
|
|
129
125
|
end
|
|
130
126
|
|
|
131
127
|
# Check for NaN or Infinity
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
end
|
|
128
|
+
return unless embedding.any? { |v| (v.respond_to?(:nan?) && v.nan?) || (v.respond_to?(:infinite?) && v.infinite?) }
|
|
129
|
+
raise HTM::EmbeddingError, "Embedding contains NaN or Infinity values"
|
|
135
130
|
end
|
|
136
131
|
|
|
137
132
|
# Pad embedding to max_dimension with zeros
|
|
@@ -82,8 +82,8 @@ class HTM
|
|
|
82
82
|
# @param options [Hash] Recall options (timeframe, limit, strategy, etc.)
|
|
83
83
|
# @return [Array<Hash>] Matching memories
|
|
84
84
|
#
|
|
85
|
-
def recall(topic, **
|
|
86
|
-
htm.recall(topic, **
|
|
85
|
+
def recall(topic, **)
|
|
86
|
+
htm.recall(topic, **)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
# JSON response helper
|
|
@@ -174,11 +174,11 @@ module ::Sinatra
|
|
|
174
174
|
config.logger = logger if respond_to?(:logger)
|
|
175
175
|
|
|
176
176
|
# Use Sidekiq if available, otherwise thread-based
|
|
177
|
-
if defined?(::Sidekiq)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
177
|
+
config.job.backend = if defined?(::Sidekiq)
|
|
178
|
+
:sidekiq
|
|
179
|
+
else
|
|
180
|
+
:thread
|
|
181
|
+
end
|
|
182
182
|
end
|
|
183
183
|
|
|
184
184
|
# Establish initial connection (Sequel handles pooling automatically)
|
data/lib/htm/job_adapter.rb
CHANGED
|
@@ -84,21 +84,17 @@ class HTM
|
|
|
84
84
|
|
|
85
85
|
# Execute job inline (synchronously)
|
|
86
86
|
def enqueue_inline(job_class, **params)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
HTM.logger.error "Inline job #{job_class.name} failed: #{e.class.name} - #{e.message}"
|
|
91
|
-
end
|
|
87
|
+
job_class.perform(**params)
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
HTM.logger.error "Inline job #{job_class.name} failed: #{e.class.name} - #{e.message}"
|
|
92
90
|
end
|
|
93
91
|
|
|
94
92
|
# Execute job in background thread (legacy)
|
|
95
93
|
def enqueue_thread(job_class, **params)
|
|
96
94
|
Thread.new do
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
HTM.logger.error "Thread job #{job_class.name} failed: #{e.class.name} - #{e.message}"
|
|
101
|
-
end
|
|
95
|
+
job_class.perform(**params)
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
HTM.logger.error "Thread job #{job_class.name} failed: #{e.class.name} - #{e.message}"
|
|
102
98
|
end
|
|
103
99
|
rescue StandardError => e
|
|
104
100
|
HTM.logger.error "Failed to start thread for #{job_class.name}: #{e.message}"
|
|
@@ -108,11 +104,9 @@ class HTM
|
|
|
108
104
|
# Non-blocking for I/O-bound operations like LLM API calls
|
|
109
105
|
def enqueue_fiber(job_class, **params)
|
|
110
106
|
Async do
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
HTM.logger.error "Fiber job #{job_class.name} failed: #{e.class.name} - #{e.message}"
|
|
115
|
-
end
|
|
107
|
+
job_class.perform(**params)
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
HTM.logger.error "Fiber job #{job_class.name} failed: #{e.class.name} - #{e.message}"
|
|
116
110
|
end
|
|
117
111
|
rescue StandardError => e
|
|
118
112
|
HTM.logger.error "Failed to start fiber for #{job_class.name}: #{e.message}"
|
|
@@ -153,16 +147,14 @@ class HTM
|
|
|
153
147
|
|
|
154
148
|
# Execute multiple jobs in parallel using async fibers
|
|
155
149
|
def enqueue_parallel_fiber(jobs)
|
|
156
|
-
Async do |
|
|
150
|
+
Async do |_task|
|
|
157
151
|
barrier = Async::Barrier.new
|
|
158
152
|
|
|
159
153
|
jobs.each do |job_class, params|
|
|
160
154
|
barrier.async do
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
HTM.logger.error "Parallel fiber job #{job_class.name} failed: #{e.class.name} - #{e.message}"
|
|
165
|
-
end
|
|
155
|
+
job_class.perform(**params)
|
|
156
|
+
rescue StandardError => e
|
|
157
|
+
HTM.logger.error "Parallel fiber job #{job_class.name} failed: #{e.class.name} - #{e.message}"
|
|
166
158
|
end
|
|
167
159
|
end
|
|
168
160
|
|
|
@@ -202,6 +194,7 @@ class HTM
|
|
|
202
194
|
# and convert string keys back to symbols for the underlying job
|
|
203
195
|
Class.new do
|
|
204
196
|
include Sidekiq::Worker
|
|
197
|
+
|
|
205
198
|
sidekiq_options queue: :htm, retry: 3
|
|
206
199
|
|
|
207
200
|
define_method(:perform) do |params|
|