htm 0.0.18 → 0.0.30
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/CHANGELOG.md +119 -1
- data/README.md +12 -0
- data/Rakefile +104 -18
- data/db/migrate/00001_enable_extensions.rb +9 -5
- data/db/migrate/00002_create_robots.rb +18 -6
- data/db/migrate/00003_create_file_sources.rb +30 -17
- data/db/migrate/00004_create_nodes.rb +60 -48
- data/db/migrate/00005_create_tags.rb +24 -12
- data/db/migrate/00006_create_node_tags.rb +28 -13
- data/db/migrate/00007_create_robot_nodes.rb +40 -26
- data/db/schema.sql +17 -1
- data/db/seeds.rb +34 -34
- data/docs/api/embedding-service.md +140 -110
- data/docs/api/yard/HTM/ActiveRecordConfig.md +6 -0
- data/docs/api/yard/HTM/Config.md +173 -0
- data/docs/api/yard/HTM/ConfigSection.md +28 -0
- data/docs/api/yard/HTM/Database.md +1 -1
- data/docs/api/yard/HTM/Railtie.md +2 -2
- data/docs/api/yard/HTM.md +0 -57
- data/docs/api/yard/index.csv +76 -61
- data/docs/api/yard-reference.md +2 -1
- data/docs/architecture/adrs/003-ollama-embeddings.md +45 -36
- data/docs/architecture/adrs/004-hive-mind.md +1 -1
- data/docs/architecture/adrs/008-robot-identification.md +1 -1
- data/docs/architecture/index.md +11 -9
- data/docs/architecture/overview.md +11 -7
- data/docs/assets/images/balanced-strategy-decay.svg +41 -0
- data/docs/assets/images/class-hierarchy.svg +1 -1
- data/docs/assets/images/eviction-priority.svg +43 -0
- data/docs/assets/images/exception-hierarchy.svg +2 -2
- data/docs/assets/images/hive-mind-shared-memory.svg +52 -0
- data/docs/assets/images/htm-architecture-overview.svg +3 -3
- data/docs/assets/images/htm-core-components.svg +4 -4
- data/docs/assets/images/htm-layered-architecture.svg +1 -1
- data/docs/assets/images/htm-memory-addition-flow.svg +2 -2
- data/docs/assets/images/htm-memory-recall-flow.svg +2 -2
- data/docs/assets/images/memory-topology.svg +53 -0
- data/docs/assets/images/two-tier-memory-architecture.svg +55 -0
- data/docs/database/naming-convention.md +244 -0
- data/docs/database_rake_tasks.md +31 -0
- data/docs/development/rake-tasks.md +80 -35
- data/docs/development/setup.md +76 -44
- data/docs/examples/basic-usage.md +133 -0
- data/docs/examples/config-files.md +170 -0
- data/docs/examples/file-loading.md +208 -0
- data/docs/examples/index.md +116 -0
- data/docs/examples/llm-configuration.md +168 -0
- data/docs/examples/mcp-client.md +172 -0
- data/docs/examples/rails-integration.md +173 -0
- data/docs/examples/robot-groups.md +210 -0
- data/docs/examples/sinatra-integration.md +218 -0
- data/docs/examples/standalone-app.md +216 -0
- data/docs/examples/telemetry.md +224 -0
- data/docs/examples/timeframes.md +143 -0
- data/docs/getting-started/installation.md +97 -40
- data/docs/getting-started/quick-start.md +28 -11
- data/docs/guides/configuration.md +515 -0
- data/docs/guides/file-loading.md +322 -0
- data/docs/guides/getting-started.md +40 -9
- data/docs/guides/index.md +3 -3
- data/docs/guides/mcp-server.md +100 -13
- data/docs/guides/propositions.md +264 -0
- data/docs/guides/recalling-memories.md +4 -4
- data/docs/guides/search-strategies.md +3 -3
- data/docs/guides/tags.md +318 -0
- data/docs/guides/telemetry.md +229 -0
- data/docs/index.md +8 -16
- data/docs/{architecture → robots}/hive-mind.md +8 -111
- data/docs/robots/index.md +73 -0
- data/docs/{guides → robots}/multi-robot.md +3 -3
- data/docs/{guides → robots}/robot-groups.md +8 -7
- data/docs/{architecture → robots}/two-tier-memory.md +13 -149
- data/docs/robots/why-robots.md +85 -0
- data/examples/.envrc +6 -0
- data/examples/.gitignore +2 -0
- data/examples/00_create_examples_db.rb +94 -0
- data/examples/{basic_usage.rb → 01_basic_usage.rb} +12 -16
- data/examples/{custom_llm_configuration.rb → 03_custom_llm_configuration.rb} +13 -3
- data/examples/{file_loader_usage.rb → 04_file_loader_usage.rb} +11 -14
- data/examples/{timeframe_demo.rb → 05_timeframe_demo.rb} +10 -3
- data/examples/{example_app → 06_example_app}/app.rb +15 -15
- data/examples/{cli_app → 07_cli_app}/htm_cli.rb +15 -22
- data/examples/08_sinatra_app/Gemfile.lock +241 -0
- data/examples/{sinatra_app → 08_sinatra_app}/app.rb +19 -18
- data/examples/{mcp_client.rb → 09_mcp_client.rb} +5 -8
- data/examples/{telemetry → 10_telemetry}/SETUP_README.md +1 -1
- data/examples/{telemetry → 10_telemetry}/demo.rb +14 -10
- data/examples/11_robot_groups/README.md +335 -0
- data/examples/{robot_groups → 11_robot_groups/lib}/robot_worker.rb +17 -3
- data/examples/{robot_groups → 11_robot_groups}/multi_process.rb +9 -9
- data/examples/{robot_groups → 11_robot_groups}/same_process.rb +9 -12
- data/examples/{rails_app → 12_rails_app}/Gemfile +3 -0
- data/examples/{rails_app → 12_rails_app}/Gemfile.lock +87 -58
- data/examples/{rails_app → 12_rails_app}/app/controllers/dashboard_controller.rb +10 -6
- data/examples/{rails_app → 12_rails_app}/app/controllers/files_controller.rb +5 -5
- data/examples/{rails_app → 12_rails_app}/app/controllers/memories_controller.rb +11 -7
- data/examples/{rails_app → 12_rails_app}/app/controllers/robots_controller.rb +8 -8
- data/examples/12_rails_app/app/controllers/tags_controller.rb +36 -0
- data/examples/{rails_app → 12_rails_app}/app/views/dashboard/index.html.erb +2 -2
- data/examples/{rails_app → 12_rails_app}/app/views/files/new.html.erb +5 -2
- data/examples/{rails_app → 12_rails_app}/app/views/memories/_memory_card.html.erb +3 -3
- data/examples/{rails_app → 12_rails_app}/app/views/memories/deleted.html.erb +3 -3
- data/examples/{rails_app → 12_rails_app}/app/views/memories/edit.html.erb +3 -3
- data/examples/{rails_app → 12_rails_app}/app/views/memories/show.html.erb +4 -4
- data/examples/{rails_app → 12_rails_app}/app/views/robots/index.html.erb +2 -2
- data/examples/{rails_app → 12_rails_app}/app/views/robots/show.html.erb +4 -4
- data/examples/{rails_app → 12_rails_app}/app/views/search/index.html.erb +1 -1
- data/examples/{rails_app → 12_rails_app}/app/views/tags/index.html.erb +2 -2
- data/examples/{rails_app → 12_rails_app}/app/views/tags/show.html.erb +1 -1
- data/examples/12_rails_app/config/initializers/htm.rb +7 -0
- data/examples/12_rails_app/config/initializers/rack.rb +5 -0
- data/examples/README.md +230 -211
- data/examples/examples_helper.rb +138 -0
- data/lib/htm/config/builder.rb +167 -0
- data/lib/htm/config/database.rb +317 -0
- data/lib/htm/config/defaults.yml +41 -13
- data/lib/htm/config/section.rb +74 -0
- data/lib/htm/config/validator.rb +83 -0
- data/lib/htm/config.rb +65 -361
- data/lib/htm/database.rb +85 -127
- data/lib/htm/errors.rb +14 -0
- data/lib/htm/integrations/sinatra.rb +13 -44
- data/lib/htm/job_adapter.rb +75 -1
- data/lib/htm/jobs/generate_embedding_job.rb +3 -4
- data/lib/htm/jobs/generate_propositions_job.rb +4 -5
- data/lib/htm/jobs/generate_tags_job.rb +16 -15
- data/lib/htm/loaders/defaults_loader.rb +23 -0
- data/lib/htm/loaders/markdown_loader.rb +17 -15
- data/lib/htm/loaders/xdg_config_loader.rb +9 -9
- data/lib/htm/long_term_memory/fulltext_search.rb +14 -14
- data/lib/htm/long_term_memory/hybrid_search.rb +396 -229
- data/lib/htm/long_term_memory/node_operations.rb +24 -23
- data/lib/htm/long_term_memory/relevance_scorer.rb +23 -20
- data/lib/htm/long_term_memory/robot_operations.rb +4 -4
- data/lib/htm/long_term_memory/tag_operations.rb +91 -77
- data/lib/htm/long_term_memory/vector_search.rb +4 -5
- data/lib/htm/long_term_memory.rb +13 -13
- data/lib/htm/mcp/cli.rb +115 -8
- data/lib/htm/mcp/resources.rb +4 -3
- data/lib/htm/mcp/server.rb +5 -4
- data/lib/htm/mcp/tools.rb +37 -28
- data/lib/htm/migration.rb +72 -0
- data/lib/htm/models/file_source.rb +52 -31
- data/lib/htm/models/node.rb +224 -108
- data/lib/htm/models/node_tag.rb +49 -28
- data/lib/htm/models/robot.rb +38 -27
- data/lib/htm/models/robot_node.rb +63 -35
- data/lib/htm/models/tag.rb +126 -123
- data/lib/htm/observability.rb +45 -41
- data/lib/htm/proposition_service.rb +76 -7
- data/lib/htm/railtie.rb +2 -2
- data/lib/htm/robot_group.rb +30 -18
- data/lib/htm/sequel_config.rb +215 -0
- data/lib/htm/sql_builder.rb +14 -16
- data/lib/htm/tag_service.rb +78 -0
- data/lib/htm/tasks.rb +3 -0
- data/lib/htm/version.rb +1 -1
- data/lib/htm/workflows/remember_workflow.rb +213 -0
- data/lib/htm.rb +27 -22
- data/lib/tasks/db.rake +0 -2
- data/lib/tasks/doc.rake +2 -2
- data/lib/tasks/files.rake +11 -18
- data/lib/tasks/htm.rake +190 -62
- data/lib/tasks/jobs.rake +179 -54
- data/lib/tasks/tags.rake +8 -13
- data/mkdocs.yml +33 -8
- data/scripts/backfill_parent_tags.rb +376 -0
- data/scripts/normalize_plural_tags.rb +335 -0
- metadata +168 -86
- data/docs/api/yard/HTM/Configuration.md +0 -240
- data/docs/telemetry.md +0 -391
- data/examples/rails_app/app/controllers/tags_controller.rb +0 -30
- data/examples/sinatra_app/Gemfile.lock +0 -166
- data/lib/htm/active_record_config.rb +0 -104
- /data/examples/{config_file_example → 02_config_file_example}/README.md +0 -0
- /data/examples/{config_file_example → 02_config_file_example}/config/htm.local.yml +0 -0
- /data/examples/{config_file_example → 02_config_file_example}/custom_config.yml +0 -0
- /data/examples/{config_file_example → 02_config_file_example}/show_config.rb +0 -0
- /data/examples/{example_app → 06_example_app}/Rakefile +0 -0
- /data/examples/{cli_app → 07_cli_app}/README.md +0 -0
- /data/examples/{sinatra_app → 08_sinatra_app}/Gemfile +0 -0
- /data/examples/{telemetry → 10_telemetry}/README.md +0 -0
- /data/examples/{telemetry → 10_telemetry}/grafana/dashboards/htm-metrics.json +0 -0
- /data/examples/{rails_app → 12_rails_app}/.gitignore +0 -0
- /data/examples/{rails_app → 12_rails_app}/Procfile.dev +0 -0
- /data/examples/{rails_app → 12_rails_app}/README.md +0 -0
- /data/examples/{rails_app → 12_rails_app}/Rakefile +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/assets/stylesheets/application.css +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/assets/stylesheets/inter-font.css +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/controllers/application_controller.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/controllers/search_controller.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/javascript/application.js +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/javascript/controllers/application.js +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/javascript/controllers/index.js +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/files/index.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/files/show.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/layouts/application.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/memories/index.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/memories/new.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/robots/new.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/shared/_navbar.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/shared/_stat_card.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/bin/dev +0 -0
- /data/examples/{rails_app → 12_rails_app}/bin/rails +0 -0
- /data/examples/{rails_app → 12_rails_app}/bin/rake +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/application.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/boot.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/database.yml +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/environment.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/importmap.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/routes.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/tailwind.config.js +0 -0
- /data/examples/{rails_app → 12_rails_app}/config.ru +0 -0
- /data/examples/{rails_app → 12_rails_app}/log/.keep +0 -0
- /data/examples/{rails_app → 12_rails_app}/tmp/local_secret.txt +0 -0
data/lib/htm/observability.rb
CHANGED
|
@@ -58,7 +58,7 @@ class HTM
|
|
|
58
58
|
query_timings: query_timing_stats,
|
|
59
59
|
service_timings: service_timing_stats,
|
|
60
60
|
memory_usage: memory_stats,
|
|
61
|
-
collected_at: Time.
|
|
61
|
+
collected_at: Time.now
|
|
62
62
|
}
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -74,38 +74,42 @@ class HTM
|
|
|
74
74
|
# - :wait_timeout - Connection wait timeout (ms)
|
|
75
75
|
#
|
|
76
76
|
def connection_pool_stats
|
|
77
|
-
return { status: :unavailable, message: "
|
|
78
|
-
|
|
79
|
-
pool =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
77
|
+
return { status: :unavailable, message: "Database not connected" } unless connected?
|
|
78
|
+
|
|
79
|
+
pool = HTM.db.pool
|
|
80
|
+
|
|
81
|
+
# Sequel's TimedQueueConnectionPool API:
|
|
82
|
+
# - max_size: maximum pool size
|
|
83
|
+
# - size: number of connections currently in pool (pre-allocated up to max_size)
|
|
84
|
+
# - num_waiting: threads waiting for connections
|
|
85
|
+
#
|
|
86
|
+
# Unlike ActiveRecord, Sequel pre-allocates connections. The key health indicator
|
|
87
|
+
# is num_waiting - if threads are waiting, the pool is under stress.
|
|
88
|
+
max_size = pool.max_size
|
|
89
|
+
current_size = pool.size
|
|
90
|
+
waiting = pool.num_waiting
|
|
91
|
+
|
|
92
|
+
# For Sequel's TimedQueueConnectionPool:
|
|
93
|
+
# - Pool is healthy if no threads are waiting
|
|
94
|
+
# - Pool is critical only if threads are waiting for connections
|
|
95
|
+
# - size == max_size is normal (pre-allocated pool), not a problem
|
|
96
|
+
status = if waiting > 0
|
|
97
|
+
waiting > max_size / 2 ? :exhausted : :critical
|
|
97
98
|
else
|
|
98
99
|
:healthy
|
|
99
100
|
end
|
|
100
101
|
|
|
102
|
+
# Utilization based on waiting threads (pool stress indicator)
|
|
103
|
+
utilization = waiting > 0 ? ((waiting.to_f / max_size) * 100).round(2) : 0.0
|
|
104
|
+
|
|
101
105
|
stats = {
|
|
102
|
-
size:
|
|
103
|
-
connections:
|
|
104
|
-
in_use:
|
|
105
|
-
available: available
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
size: max_size,
|
|
107
|
+
connections: current_size,
|
|
108
|
+
in_use: 0, # Sequel doesn't expose checked-out count; use waiting as stress indicator
|
|
109
|
+
available: max_size, # All connections are available when not waiting
|
|
110
|
+
waiting: waiting,
|
|
111
|
+
utilization: utilization,
|
|
112
|
+
status: status
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
# Log warnings if pool is stressed
|
|
@@ -170,7 +174,7 @@ class HTM
|
|
|
170
174
|
@query_timings << {
|
|
171
175
|
duration_ms: duration_ms,
|
|
172
176
|
query_type: query_type,
|
|
173
|
-
recorded_at: Time.
|
|
177
|
+
recorded_at: Time.now
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
# Keep only recent samples
|
|
@@ -186,7 +190,7 @@ class HTM
|
|
|
186
190
|
@metrics_mutex.synchronize do
|
|
187
191
|
@embedding_timings << {
|
|
188
192
|
duration_ms: duration_ms,
|
|
189
|
-
recorded_at: Time.
|
|
193
|
+
recorded_at: Time.now
|
|
190
194
|
}
|
|
191
195
|
@embedding_timings.shift if @embedding_timings.size > @max_timing_samples
|
|
192
196
|
end
|
|
@@ -200,7 +204,7 @@ class HTM
|
|
|
200
204
|
@metrics_mutex.synchronize do
|
|
201
205
|
@tag_extraction_timings << {
|
|
202
206
|
duration_ms: duration_ms,
|
|
203
|
-
recorded_at: Time.
|
|
207
|
+
recorded_at: Time.now
|
|
204
208
|
}
|
|
205
209
|
@tag_extraction_timings.shift if @tag_extraction_timings.size > @max_timing_samples
|
|
206
210
|
end
|
|
@@ -287,7 +291,7 @@ class HTM
|
|
|
287
291
|
healthy: issues.empty?,
|
|
288
292
|
checks: checks,
|
|
289
293
|
issues: issues,
|
|
290
|
-
checked_at: Time.
|
|
294
|
+
checked_at: Time.now
|
|
291
295
|
}
|
|
292
296
|
end
|
|
293
297
|
|
|
@@ -313,22 +317,22 @@ class HTM
|
|
|
313
317
|
|
|
314
318
|
private
|
|
315
319
|
|
|
316
|
-
# Check if
|
|
320
|
+
# Check if Sequel database is connected
|
|
317
321
|
def connected?
|
|
318
|
-
return false unless defined?(
|
|
319
|
-
|
|
322
|
+
return false unless defined?(HTM) && HTM.respond_to?(:db)
|
|
323
|
+
db = HTM.db
|
|
324
|
+
return false unless db
|
|
325
|
+
db.test_connection
|
|
320
326
|
rescue StandardError
|
|
321
327
|
false
|
|
322
328
|
end
|
|
323
329
|
|
|
324
330
|
# Check if a PostgreSQL extension is installed
|
|
325
331
|
def extension_installed?(name)
|
|
326
|
-
result =
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
)
|
|
331
|
-
result.to_i > 0
|
|
332
|
+
result = HTM.db.fetch(
|
|
333
|
+
"SELECT COUNT(*) AS cnt FROM pg_extension WHERE extname = ?", name
|
|
334
|
+
).first
|
|
335
|
+
result[:cnt].to_i > 0
|
|
332
336
|
end
|
|
333
337
|
|
|
334
338
|
# Calculate timing statistics from samples
|
|
@@ -26,8 +26,20 @@ class HTM
|
|
|
26
26
|
# # "The Apollo 11 mission occurred in 1969."]
|
|
27
27
|
#
|
|
28
28
|
class PropositionService
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
# Patterns that indicate meta-responses (LLM asking for input instead of extracting)
|
|
30
|
+
META_RESPONSE_PATTERNS = [
|
|
31
|
+
/please provide/i,
|
|
32
|
+
/provide the text/i,
|
|
33
|
+
/provide me with/i,
|
|
34
|
+
/I need the text/i,
|
|
35
|
+
/I am ready/i,
|
|
36
|
+
/waiting for/i,
|
|
37
|
+
/send me the/i,
|
|
38
|
+
/what text would you/i,
|
|
39
|
+
/what would you like/i,
|
|
40
|
+
/cannot extract.*without/i,
|
|
41
|
+
/no text provided/i
|
|
42
|
+
].freeze
|
|
31
43
|
|
|
32
44
|
# Circuit breaker for proposition extraction API calls
|
|
33
45
|
@circuit_breaker = nil
|
|
@@ -112,6 +124,45 @@ class HTM
|
|
|
112
124
|
end
|
|
113
125
|
end
|
|
114
126
|
|
|
127
|
+
# Get minimum character length from config
|
|
128
|
+
#
|
|
129
|
+
# @return [Integer] Minimum character count for valid propositions
|
|
130
|
+
#
|
|
131
|
+
def self.min_length
|
|
132
|
+
HTM.config.proposition.min_length || 10
|
|
133
|
+
rescue
|
|
134
|
+
10
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Get maximum character length from config
|
|
138
|
+
#
|
|
139
|
+
# @return [Integer] Maximum character count for valid propositions
|
|
140
|
+
#
|
|
141
|
+
def self.max_length
|
|
142
|
+
HTM.config.proposition.max_length || 1000
|
|
143
|
+
rescue
|
|
144
|
+
1000
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Get minimum words from config
|
|
148
|
+
#
|
|
149
|
+
# @return [Integer] Minimum word count for valid propositions
|
|
150
|
+
#
|
|
151
|
+
def self.min_words
|
|
152
|
+
HTM.config.proposition.min_words || 5
|
|
153
|
+
rescue
|
|
154
|
+
5
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Check if proposition is a meta-response (LLM asking for input)
|
|
158
|
+
#
|
|
159
|
+
# @param proposition [String] Proposition to check
|
|
160
|
+
# @return [Boolean] True if it's a meta-response
|
|
161
|
+
#
|
|
162
|
+
def self.meta_response?(proposition)
|
|
163
|
+
META_RESPONSE_PATTERNS.any? { |pattern| proposition.match?(pattern) }
|
|
164
|
+
end
|
|
165
|
+
|
|
115
166
|
# Validate and filter propositions
|
|
116
167
|
#
|
|
117
168
|
# @param propositions [Array<String>] Parsed propositions
|
|
@@ -119,13 +170,16 @@ class HTM
|
|
|
119
170
|
#
|
|
120
171
|
def self.validate_and_filter_propositions(propositions)
|
|
121
172
|
valid_propositions = []
|
|
173
|
+
min_char_length = min_length
|
|
174
|
+
max_char_length = max_length
|
|
175
|
+
min_word_count = min_words
|
|
122
176
|
|
|
123
177
|
propositions.each do |proposition|
|
|
124
|
-
# Check minimum length
|
|
125
|
-
next if proposition.length <
|
|
178
|
+
# Check minimum length (characters)
|
|
179
|
+
next if proposition.length < min_char_length
|
|
126
180
|
|
|
127
181
|
# Check maximum length
|
|
128
|
-
if proposition.length >
|
|
182
|
+
if proposition.length > max_char_length
|
|
129
183
|
HTM.logger.warn "PropositionService: Proposition too long, skipping: #{proposition[0..50]}..."
|
|
130
184
|
next
|
|
131
185
|
end
|
|
@@ -135,6 +189,19 @@ class HTM
|
|
|
135
189
|
next
|
|
136
190
|
end
|
|
137
191
|
|
|
192
|
+
# Check minimum word count
|
|
193
|
+
word_count = proposition.split.size
|
|
194
|
+
if word_count < min_word_count
|
|
195
|
+
HTM.logger.debug "PropositionService: Proposition too short (#{word_count} words), skipping: #{proposition}"
|
|
196
|
+
next
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Filter out meta-responses (LLM asking for more input)
|
|
200
|
+
if meta_response?(proposition)
|
|
201
|
+
HTM.logger.warn "PropositionService: Filtered meta-response: #{proposition[0..50]}..."
|
|
202
|
+
next
|
|
203
|
+
end
|
|
204
|
+
|
|
138
205
|
# Proposition is valid
|
|
139
206
|
valid_propositions << proposition
|
|
140
207
|
end
|
|
@@ -149,9 +216,11 @@ class HTM
|
|
|
149
216
|
#
|
|
150
217
|
def self.valid_proposition?(proposition)
|
|
151
218
|
return false unless proposition.is_a?(String)
|
|
152
|
-
return false if proposition.length <
|
|
153
|
-
return false if proposition.length >
|
|
219
|
+
return false if proposition.length < min_length
|
|
220
|
+
return false if proposition.length > max_length
|
|
154
221
|
return false unless proposition.match?(/[a-zA-Z]{3,}/)
|
|
222
|
+
return false if proposition.split.size < min_words
|
|
223
|
+
return false if meta_response?(proposition)
|
|
155
224
|
|
|
156
225
|
true
|
|
157
226
|
end
|
data/lib/htm/railtie.rb
CHANGED
|
@@ -57,8 +57,8 @@ class HTM
|
|
|
57
57
|
config.after_initialize do
|
|
58
58
|
if Rails.env.development?
|
|
59
59
|
begin
|
|
60
|
-
HTM::
|
|
61
|
-
HTM::
|
|
60
|
+
HTM::SequelConfig.establish_connection! unless HTM::SequelConfig.db
|
|
61
|
+
HTM::SequelConfig.verify_extensions!
|
|
62
62
|
HTM.logger.info "HTM database connection verified"
|
|
63
63
|
rescue StandardError => e
|
|
64
64
|
HTM.logger.warn "HTM database connection check failed: #{e.message}"
|
data/lib/htm/robot_group.rb
CHANGED
|
@@ -180,7 +180,7 @@ class HTM
|
|
|
180
180
|
# Clear working memory flags for this robot
|
|
181
181
|
HTM::Models::RobotNode
|
|
182
182
|
.where(robot_id: htm.robot_id, working_memory: true)
|
|
183
|
-
.
|
|
183
|
+
.update(working_memory: false)
|
|
184
184
|
end
|
|
185
185
|
|
|
186
186
|
# Promotes a passive robot to active status.
|
|
@@ -368,7 +368,7 @@ class HTM
|
|
|
368
368
|
# Queries the database for the union of all members' working memory,
|
|
369
369
|
# returning nodes sorted by creation date (newest first).
|
|
370
370
|
#
|
|
371
|
-
# @return [
|
|
371
|
+
# @return [Sequel::Dataset] Collection of nodes
|
|
372
372
|
#
|
|
373
373
|
# @example
|
|
374
374
|
# nodes = group.working_memory_contents
|
|
@@ -378,9 +378,9 @@ class HTM
|
|
|
378
378
|
node_ids = HTM::Models::RobotNode
|
|
379
379
|
.where(robot_id: member_ids, working_memory: true)
|
|
380
380
|
.distinct
|
|
381
|
-
.
|
|
381
|
+
.select_map(:node_id)
|
|
382
382
|
|
|
383
|
-
HTM::Models::Node.where(id: node_ids).order(created_at
|
|
383
|
+
HTM::Models::Node.where(id: node_ids).order(Sequel.desc(:created_at))
|
|
384
384
|
end
|
|
385
385
|
|
|
386
386
|
# Clears shared working memory for all group members.
|
|
@@ -397,7 +397,7 @@ class HTM
|
|
|
397
397
|
def clear_working_memory
|
|
398
398
|
count = HTM::Models::RobotNode
|
|
399
399
|
.where(robot_id: member_ids, working_memory: true)
|
|
400
|
-
.
|
|
400
|
+
.update(working_memory: false)
|
|
401
401
|
|
|
402
402
|
# Clear in-memory working memory for primary robot
|
|
403
403
|
primary = @active_robots.values.first || @passive_robots.values.first
|
|
@@ -435,21 +435,25 @@ class HTM
|
|
|
435
435
|
# Get all node_ids currently in any member's working memory
|
|
436
436
|
shared_node_ids = HTM::Models::RobotNode
|
|
437
437
|
.where(robot_id: member_ids, working_memory: true)
|
|
438
|
-
.
|
|
438
|
+
.exclude(robot_id: htm.robot_id)
|
|
439
439
|
.distinct
|
|
440
|
-
.
|
|
440
|
+
.select_map(:node_id)
|
|
441
441
|
|
|
442
442
|
synced = 0
|
|
443
443
|
shared_node_ids.each do |node_id|
|
|
444
444
|
# Create or update robot_node with working_memory=true
|
|
445
|
-
robot_node = HTM::Models::RobotNode.
|
|
445
|
+
robot_node = HTM::Models::RobotNode.first(
|
|
446
446
|
robot_id: htm.robot_id,
|
|
447
447
|
node_id: node_id
|
|
448
448
|
)
|
|
449
|
-
|
|
449
|
+
robot_node ||= HTM::Models::RobotNode.new(
|
|
450
|
+
robot_id: htm.robot_id,
|
|
451
|
+
node_id: node_id
|
|
452
|
+
)
|
|
453
|
+
next if robot_node.working_memory
|
|
450
454
|
|
|
451
455
|
robot_node.working_memory = true
|
|
452
|
-
robot_node.save
|
|
456
|
+
robot_node.save
|
|
453
457
|
synced += 1
|
|
454
458
|
end
|
|
455
459
|
|
|
@@ -501,7 +505,7 @@ class HTM
|
|
|
501
505
|
working_memories = member_ids.map do |robot_id|
|
|
502
506
|
HTM::Models::RobotNode
|
|
503
507
|
.where(robot_id: robot_id, working_memory: true)
|
|
504
|
-
.
|
|
508
|
+
.select_map(:node_id)
|
|
505
509
|
.sort
|
|
506
510
|
end
|
|
507
511
|
|
|
@@ -543,16 +547,20 @@ class HTM
|
|
|
543
547
|
# Get source's working memory nodes
|
|
544
548
|
source_node_ids = HTM::Models::RobotNode
|
|
545
549
|
.where(robot_id: from_htm.robot_id, working_memory: true)
|
|
546
|
-
.
|
|
550
|
+
.select_map(:node_id)
|
|
547
551
|
|
|
548
552
|
transferred = 0
|
|
549
553
|
source_node_ids.each do |node_id|
|
|
550
|
-
robot_node = HTM::Models::RobotNode.
|
|
554
|
+
robot_node = HTM::Models::RobotNode.first(
|
|
555
|
+
robot_id: to_htm.robot_id,
|
|
556
|
+
node_id: node_id
|
|
557
|
+
)
|
|
558
|
+
robot_node ||= HTM::Models::RobotNode.new(
|
|
551
559
|
robot_id: to_htm.robot_id,
|
|
552
560
|
node_id: node_id
|
|
553
561
|
)
|
|
554
562
|
robot_node.working_memory = true
|
|
555
|
-
robot_node.save
|
|
563
|
+
robot_node.save
|
|
556
564
|
transferred += 1
|
|
557
565
|
end
|
|
558
566
|
|
|
@@ -560,7 +568,7 @@ class HTM
|
|
|
560
568
|
if clear_source
|
|
561
569
|
HTM::Models::RobotNode
|
|
562
570
|
.where(robot_id: from_htm.robot_id, working_memory: true)
|
|
563
|
-
.
|
|
571
|
+
.update(working_memory: false)
|
|
564
572
|
end
|
|
565
573
|
|
|
566
574
|
transferred
|
|
@@ -674,7 +682,7 @@ class HTM
|
|
|
674
682
|
end
|
|
675
683
|
|
|
676
684
|
def sync_node_to_in_memory_caches(node_id, origin_robot_id)
|
|
677
|
-
node = HTM::Models::Node.
|
|
685
|
+
node = HTM::Models::Node.first(id: node_id)
|
|
678
686
|
return unless node
|
|
679
687
|
|
|
680
688
|
all_robots.each do |_name, htm|
|
|
@@ -709,12 +717,16 @@ class HTM
|
|
|
709
717
|
member_ids.each do |robot_id|
|
|
710
718
|
next if robot_id == exclude
|
|
711
719
|
|
|
712
|
-
robot_node = HTM::Models::RobotNode.
|
|
720
|
+
robot_node = HTM::Models::RobotNode.first(
|
|
721
|
+
robot_id: robot_id,
|
|
722
|
+
node_id: node_id
|
|
723
|
+
)
|
|
724
|
+
robot_node ||= HTM::Models::RobotNode.new(
|
|
713
725
|
robot_id: robot_id,
|
|
714
726
|
node_id: node_id
|
|
715
727
|
)
|
|
716
728
|
robot_node.working_memory = true
|
|
717
|
-
robot_node.save
|
|
729
|
+
robot_node.save
|
|
718
730
|
end
|
|
719
731
|
end
|
|
720
732
|
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sequel'
|
|
4
|
+
|
|
5
|
+
class HTM
|
|
6
|
+
# Sequel database configuration and model loading
|
|
7
|
+
#
|
|
8
|
+
# Uses HTM::Config for database settings. Configuration can come from:
|
|
9
|
+
# - Environment variables (HTM_DATABASE__URL, HTM_DATABASE__HOST, etc.)
|
|
10
|
+
# - Programmatic configuration via HTM.configure
|
|
11
|
+
#
|
|
12
|
+
# Sequel is fiber-safe by design, making it ideal for async/fiber-based
|
|
13
|
+
# concurrency patterns used in HTM's job processing.
|
|
14
|
+
#
|
|
15
|
+
class SequelConfig
|
|
16
|
+
class << self
|
|
17
|
+
# Database connection instance
|
|
18
|
+
# @return [Sequel::Database, nil]
|
|
19
|
+
attr_reader :db
|
|
20
|
+
|
|
21
|
+
# Establish database connection from HTM::Config
|
|
22
|
+
#
|
|
23
|
+
# @param load_models [Boolean] Whether to load models after connection (default: true)
|
|
24
|
+
# Set to false when running migrations on a fresh database
|
|
25
|
+
# @return [Sequel::Database] The database connection
|
|
26
|
+
#
|
|
27
|
+
def establish_connection!(load_models: true)
|
|
28
|
+
return @db if @db
|
|
29
|
+
|
|
30
|
+
config = load_database_config
|
|
31
|
+
connection_string = build_connection_string(config)
|
|
32
|
+
|
|
33
|
+
# Configure Sequel with fiber-safe settings
|
|
34
|
+
@db = Sequel.connect(connection_string, {
|
|
35
|
+
max_connections: config[:pool] || 5,
|
|
36
|
+
pool_timeout: (config[:checkout_timeout] || 5).to_i,
|
|
37
|
+
# Fiber-safe settings - important for async gem compatibility
|
|
38
|
+
preconnect: :concurrently,
|
|
39
|
+
# Use threaded mode which works well with fibers
|
|
40
|
+
single_threaded: false
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
# Load PostgreSQL-specific extensions for JSONB and array handling
|
|
44
|
+
@db.extension :pg_json
|
|
45
|
+
@db.extension :pg_array
|
|
46
|
+
|
|
47
|
+
# Set search path and statement timeout
|
|
48
|
+
@db.run("SET search_path TO public")
|
|
49
|
+
@db.run("SET statement_timeout = #{config[:statement_timeout] || 30_000}")
|
|
50
|
+
|
|
51
|
+
# Load models after connection is established (unless disabled for migrations)
|
|
52
|
+
require_models if load_models && models_loadable?
|
|
53
|
+
|
|
54
|
+
@db
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check if models can be loaded (tables exist)
|
|
58
|
+
#
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
#
|
|
61
|
+
def models_loadable?
|
|
62
|
+
return false unless @db
|
|
63
|
+
@db.table_exists?(:robots)
|
|
64
|
+
rescue Sequel::DatabaseError
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Ensure models are loaded
|
|
69
|
+
#
|
|
70
|
+
# Call this after migrations to ensure models are available
|
|
71
|
+
# @return [void]
|
|
72
|
+
#
|
|
73
|
+
def ensure_models_loaded!
|
|
74
|
+
require_models unless @models_loaded
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Load database configuration from HTM::Config
|
|
78
|
+
#
|
|
79
|
+
# @return [Hash] Database configuration hash
|
|
80
|
+
#
|
|
81
|
+
def load_database_config
|
|
82
|
+
HTM.config.database_config
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Build connection string from config hash
|
|
86
|
+
#
|
|
87
|
+
# @param config [Hash] Database configuration
|
|
88
|
+
# @return [String] PostgreSQL connection string
|
|
89
|
+
#
|
|
90
|
+
def build_connection_string(config)
|
|
91
|
+
# If we have a URL already, use it
|
|
92
|
+
if config[:url]
|
|
93
|
+
return config[:url]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
user = config[:username] || config[:user]
|
|
97
|
+
password = config[:password]
|
|
98
|
+
host = config[:host] || 'localhost'
|
|
99
|
+
port = config[:port] || 5432
|
|
100
|
+
database = config[:database]
|
|
101
|
+
|
|
102
|
+
if password && !password.empty?
|
|
103
|
+
"postgres://#{user}:#{password}@#{host}:#{port}/#{database}"
|
|
104
|
+
elsif user
|
|
105
|
+
"postgres://#{user}@#{host}:#{port}/#{database}"
|
|
106
|
+
else
|
|
107
|
+
"postgres://#{host}:#{port}/#{database}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Check if connection is established and active
|
|
112
|
+
#
|
|
113
|
+
# @return [Boolean]
|
|
114
|
+
#
|
|
115
|
+
def connected?
|
|
116
|
+
return false unless @db
|
|
117
|
+
|
|
118
|
+
@db.test_connection
|
|
119
|
+
rescue Sequel::DatabaseError
|
|
120
|
+
false
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Close all database connections
|
|
124
|
+
#
|
|
125
|
+
# @return [void]
|
|
126
|
+
#
|
|
127
|
+
def disconnect!
|
|
128
|
+
@db&.disconnect
|
|
129
|
+
@db = nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Verify required extensions are available
|
|
133
|
+
#
|
|
134
|
+
# @raise [RuntimeError] if required extensions are missing
|
|
135
|
+
# @return [true]
|
|
136
|
+
#
|
|
137
|
+
def verify_extensions!
|
|
138
|
+
required_extensions = {
|
|
139
|
+
'vector' => 'pgvector extension',
|
|
140
|
+
'pg_trgm' => 'PostgreSQL trigram extension'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
missing = []
|
|
144
|
+
required_extensions.each do |ext, name|
|
|
145
|
+
result = @db["SELECT COUNT(*) AS cnt FROM pg_extension WHERE extname = ?", ext].first
|
|
146
|
+
missing << name if result[:cnt].to_i.zero?
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
if missing.any?
|
|
150
|
+
raise "Missing required PostgreSQL extensions: #{missing.join(', ')}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
true
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Get connection pool statistics
|
|
157
|
+
#
|
|
158
|
+
# @return [Hash] Pool statistics
|
|
159
|
+
#
|
|
160
|
+
def connection_stats
|
|
161
|
+
pool = @db.pool
|
|
162
|
+
{
|
|
163
|
+
size: pool.max_size,
|
|
164
|
+
available: pool.available_connections.size,
|
|
165
|
+
allocated: pool.allocated.size
|
|
166
|
+
}
|
|
167
|
+
rescue NoMethodError
|
|
168
|
+
# Fallback for connection pools that don't support these methods
|
|
169
|
+
{ size: @db.pool.max_size }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Run raw SQL
|
|
173
|
+
#
|
|
174
|
+
# @param sql [String] SQL to execute
|
|
175
|
+
# @return [void]
|
|
176
|
+
#
|
|
177
|
+
def execute(sql)
|
|
178
|
+
@db.run(sql)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Select a single value
|
|
182
|
+
#
|
|
183
|
+
# @param sql [String] SQL query
|
|
184
|
+
# @return [Object] The value
|
|
185
|
+
#
|
|
186
|
+
def select_value(sql)
|
|
187
|
+
@db[sql].first&.values&.first
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
# Require all model files
|
|
193
|
+
def require_models
|
|
194
|
+
return if @models_loaded
|
|
195
|
+
|
|
196
|
+
require_relative 'models/robot'
|
|
197
|
+
require_relative 'models/node'
|
|
198
|
+
require_relative 'models/robot_node'
|
|
199
|
+
require_relative 'models/tag'
|
|
200
|
+
require_relative 'models/node_tag'
|
|
201
|
+
require_relative 'models/file_source'
|
|
202
|
+
|
|
203
|
+
@models_loaded = true
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Convenience method to access the database connection
|
|
209
|
+
#
|
|
210
|
+
# @return [Sequel::Database]
|
|
211
|
+
#
|
|
212
|
+
def self.db
|
|
213
|
+
SequelConfig.db || SequelConfig.establish_connection!
|
|
214
|
+
end
|
|
215
|
+
end
|