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.
Files changed (216) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +119 -1
  3. data/README.md +12 -0
  4. data/Rakefile +104 -18
  5. data/db/migrate/00001_enable_extensions.rb +9 -5
  6. data/db/migrate/00002_create_robots.rb +18 -6
  7. data/db/migrate/00003_create_file_sources.rb +30 -17
  8. data/db/migrate/00004_create_nodes.rb +60 -48
  9. data/db/migrate/00005_create_tags.rb +24 -12
  10. data/db/migrate/00006_create_node_tags.rb +28 -13
  11. data/db/migrate/00007_create_robot_nodes.rb +40 -26
  12. data/db/schema.sql +17 -1
  13. data/db/seeds.rb +34 -34
  14. data/docs/api/embedding-service.md +140 -110
  15. data/docs/api/yard/HTM/ActiveRecordConfig.md +6 -0
  16. data/docs/api/yard/HTM/Config.md +173 -0
  17. data/docs/api/yard/HTM/ConfigSection.md +28 -0
  18. data/docs/api/yard/HTM/Database.md +1 -1
  19. data/docs/api/yard/HTM/Railtie.md +2 -2
  20. data/docs/api/yard/HTM.md +0 -57
  21. data/docs/api/yard/index.csv +76 -61
  22. data/docs/api/yard-reference.md +2 -1
  23. data/docs/architecture/adrs/003-ollama-embeddings.md +45 -36
  24. data/docs/architecture/adrs/004-hive-mind.md +1 -1
  25. data/docs/architecture/adrs/008-robot-identification.md +1 -1
  26. data/docs/architecture/index.md +11 -9
  27. data/docs/architecture/overview.md +11 -7
  28. data/docs/assets/images/balanced-strategy-decay.svg +41 -0
  29. data/docs/assets/images/class-hierarchy.svg +1 -1
  30. data/docs/assets/images/eviction-priority.svg +43 -0
  31. data/docs/assets/images/exception-hierarchy.svg +2 -2
  32. data/docs/assets/images/hive-mind-shared-memory.svg +52 -0
  33. data/docs/assets/images/htm-architecture-overview.svg +3 -3
  34. data/docs/assets/images/htm-core-components.svg +4 -4
  35. data/docs/assets/images/htm-layered-architecture.svg +1 -1
  36. data/docs/assets/images/htm-memory-addition-flow.svg +2 -2
  37. data/docs/assets/images/htm-memory-recall-flow.svg +2 -2
  38. data/docs/assets/images/memory-topology.svg +53 -0
  39. data/docs/assets/images/two-tier-memory-architecture.svg +55 -0
  40. data/docs/database/naming-convention.md +244 -0
  41. data/docs/database_rake_tasks.md +31 -0
  42. data/docs/development/rake-tasks.md +80 -35
  43. data/docs/development/setup.md +76 -44
  44. data/docs/examples/basic-usage.md +133 -0
  45. data/docs/examples/config-files.md +170 -0
  46. data/docs/examples/file-loading.md +208 -0
  47. data/docs/examples/index.md +116 -0
  48. data/docs/examples/llm-configuration.md +168 -0
  49. data/docs/examples/mcp-client.md +172 -0
  50. data/docs/examples/rails-integration.md +173 -0
  51. data/docs/examples/robot-groups.md +210 -0
  52. data/docs/examples/sinatra-integration.md +218 -0
  53. data/docs/examples/standalone-app.md +216 -0
  54. data/docs/examples/telemetry.md +224 -0
  55. data/docs/examples/timeframes.md +143 -0
  56. data/docs/getting-started/installation.md +97 -40
  57. data/docs/getting-started/quick-start.md +28 -11
  58. data/docs/guides/configuration.md +515 -0
  59. data/docs/guides/file-loading.md +322 -0
  60. data/docs/guides/getting-started.md +40 -9
  61. data/docs/guides/index.md +3 -3
  62. data/docs/guides/mcp-server.md +100 -13
  63. data/docs/guides/propositions.md +264 -0
  64. data/docs/guides/recalling-memories.md +4 -4
  65. data/docs/guides/search-strategies.md +3 -3
  66. data/docs/guides/tags.md +318 -0
  67. data/docs/guides/telemetry.md +229 -0
  68. data/docs/index.md +8 -16
  69. data/docs/{architecture → robots}/hive-mind.md +8 -111
  70. data/docs/robots/index.md +73 -0
  71. data/docs/{guides → robots}/multi-robot.md +3 -3
  72. data/docs/{guides → robots}/robot-groups.md +8 -7
  73. data/docs/{architecture → robots}/two-tier-memory.md +13 -149
  74. data/docs/robots/why-robots.md +85 -0
  75. data/examples/.envrc +6 -0
  76. data/examples/.gitignore +2 -0
  77. data/examples/00_create_examples_db.rb +94 -0
  78. data/examples/{basic_usage.rb → 01_basic_usage.rb} +12 -16
  79. data/examples/{custom_llm_configuration.rb → 03_custom_llm_configuration.rb} +13 -3
  80. data/examples/{file_loader_usage.rb → 04_file_loader_usage.rb} +11 -14
  81. data/examples/{timeframe_demo.rb → 05_timeframe_demo.rb} +10 -3
  82. data/examples/{example_app → 06_example_app}/app.rb +15 -15
  83. data/examples/{cli_app → 07_cli_app}/htm_cli.rb +15 -22
  84. data/examples/08_sinatra_app/Gemfile.lock +241 -0
  85. data/examples/{sinatra_app → 08_sinatra_app}/app.rb +19 -18
  86. data/examples/{mcp_client.rb → 09_mcp_client.rb} +5 -8
  87. data/examples/{telemetry → 10_telemetry}/SETUP_README.md +1 -1
  88. data/examples/{telemetry → 10_telemetry}/demo.rb +14 -10
  89. data/examples/11_robot_groups/README.md +335 -0
  90. data/examples/{robot_groups → 11_robot_groups/lib}/robot_worker.rb +17 -3
  91. data/examples/{robot_groups → 11_robot_groups}/multi_process.rb +9 -9
  92. data/examples/{robot_groups → 11_robot_groups}/same_process.rb +9 -12
  93. data/examples/{rails_app → 12_rails_app}/Gemfile +3 -0
  94. data/examples/{rails_app → 12_rails_app}/Gemfile.lock +87 -58
  95. data/examples/{rails_app → 12_rails_app}/app/controllers/dashboard_controller.rb +10 -6
  96. data/examples/{rails_app → 12_rails_app}/app/controllers/files_controller.rb +5 -5
  97. data/examples/{rails_app → 12_rails_app}/app/controllers/memories_controller.rb +11 -7
  98. data/examples/{rails_app → 12_rails_app}/app/controllers/robots_controller.rb +8 -8
  99. data/examples/12_rails_app/app/controllers/tags_controller.rb +36 -0
  100. data/examples/{rails_app → 12_rails_app}/app/views/dashboard/index.html.erb +2 -2
  101. data/examples/{rails_app → 12_rails_app}/app/views/files/new.html.erb +5 -2
  102. data/examples/{rails_app → 12_rails_app}/app/views/memories/_memory_card.html.erb +3 -3
  103. data/examples/{rails_app → 12_rails_app}/app/views/memories/deleted.html.erb +3 -3
  104. data/examples/{rails_app → 12_rails_app}/app/views/memories/edit.html.erb +3 -3
  105. data/examples/{rails_app → 12_rails_app}/app/views/memories/show.html.erb +4 -4
  106. data/examples/{rails_app → 12_rails_app}/app/views/robots/index.html.erb +2 -2
  107. data/examples/{rails_app → 12_rails_app}/app/views/robots/show.html.erb +4 -4
  108. data/examples/{rails_app → 12_rails_app}/app/views/search/index.html.erb +1 -1
  109. data/examples/{rails_app → 12_rails_app}/app/views/tags/index.html.erb +2 -2
  110. data/examples/{rails_app → 12_rails_app}/app/views/tags/show.html.erb +1 -1
  111. data/examples/12_rails_app/config/initializers/htm.rb +7 -0
  112. data/examples/12_rails_app/config/initializers/rack.rb +5 -0
  113. data/examples/README.md +230 -211
  114. data/examples/examples_helper.rb +138 -0
  115. data/lib/htm/config/builder.rb +167 -0
  116. data/lib/htm/config/database.rb +317 -0
  117. data/lib/htm/config/defaults.yml +41 -13
  118. data/lib/htm/config/section.rb +74 -0
  119. data/lib/htm/config/validator.rb +83 -0
  120. data/lib/htm/config.rb +65 -361
  121. data/lib/htm/database.rb +85 -127
  122. data/lib/htm/errors.rb +14 -0
  123. data/lib/htm/integrations/sinatra.rb +13 -44
  124. data/lib/htm/job_adapter.rb +75 -1
  125. data/lib/htm/jobs/generate_embedding_job.rb +3 -4
  126. data/lib/htm/jobs/generate_propositions_job.rb +4 -5
  127. data/lib/htm/jobs/generate_tags_job.rb +16 -15
  128. data/lib/htm/loaders/defaults_loader.rb +23 -0
  129. data/lib/htm/loaders/markdown_loader.rb +17 -15
  130. data/lib/htm/loaders/xdg_config_loader.rb +9 -9
  131. data/lib/htm/long_term_memory/fulltext_search.rb +14 -14
  132. data/lib/htm/long_term_memory/hybrid_search.rb +396 -229
  133. data/lib/htm/long_term_memory/node_operations.rb +24 -23
  134. data/lib/htm/long_term_memory/relevance_scorer.rb +23 -20
  135. data/lib/htm/long_term_memory/robot_operations.rb +4 -4
  136. data/lib/htm/long_term_memory/tag_operations.rb +91 -77
  137. data/lib/htm/long_term_memory/vector_search.rb +4 -5
  138. data/lib/htm/long_term_memory.rb +13 -13
  139. data/lib/htm/mcp/cli.rb +115 -8
  140. data/lib/htm/mcp/resources.rb +4 -3
  141. data/lib/htm/mcp/server.rb +5 -4
  142. data/lib/htm/mcp/tools.rb +37 -28
  143. data/lib/htm/migration.rb +72 -0
  144. data/lib/htm/models/file_source.rb +52 -31
  145. data/lib/htm/models/node.rb +224 -108
  146. data/lib/htm/models/node_tag.rb +49 -28
  147. data/lib/htm/models/robot.rb +38 -27
  148. data/lib/htm/models/robot_node.rb +63 -35
  149. data/lib/htm/models/tag.rb +126 -123
  150. data/lib/htm/observability.rb +45 -41
  151. data/lib/htm/proposition_service.rb +76 -7
  152. data/lib/htm/railtie.rb +2 -2
  153. data/lib/htm/robot_group.rb +30 -18
  154. data/lib/htm/sequel_config.rb +215 -0
  155. data/lib/htm/sql_builder.rb +14 -16
  156. data/lib/htm/tag_service.rb +78 -0
  157. data/lib/htm/tasks.rb +3 -0
  158. data/lib/htm/version.rb +1 -1
  159. data/lib/htm/workflows/remember_workflow.rb +213 -0
  160. data/lib/htm.rb +27 -22
  161. data/lib/tasks/db.rake +0 -2
  162. data/lib/tasks/doc.rake +2 -2
  163. data/lib/tasks/files.rake +11 -18
  164. data/lib/tasks/htm.rake +190 -62
  165. data/lib/tasks/jobs.rake +179 -54
  166. data/lib/tasks/tags.rake +8 -13
  167. data/mkdocs.yml +33 -8
  168. data/scripts/backfill_parent_tags.rb +376 -0
  169. data/scripts/normalize_plural_tags.rb +335 -0
  170. metadata +168 -86
  171. data/docs/api/yard/HTM/Configuration.md +0 -240
  172. data/docs/telemetry.md +0 -391
  173. data/examples/rails_app/app/controllers/tags_controller.rb +0 -30
  174. data/examples/sinatra_app/Gemfile.lock +0 -166
  175. data/lib/htm/active_record_config.rb +0 -104
  176. /data/examples/{config_file_example → 02_config_file_example}/README.md +0 -0
  177. /data/examples/{config_file_example → 02_config_file_example}/config/htm.local.yml +0 -0
  178. /data/examples/{config_file_example → 02_config_file_example}/custom_config.yml +0 -0
  179. /data/examples/{config_file_example → 02_config_file_example}/show_config.rb +0 -0
  180. /data/examples/{example_app → 06_example_app}/Rakefile +0 -0
  181. /data/examples/{cli_app → 07_cli_app}/README.md +0 -0
  182. /data/examples/{sinatra_app → 08_sinatra_app}/Gemfile +0 -0
  183. /data/examples/{telemetry → 10_telemetry}/README.md +0 -0
  184. /data/examples/{telemetry → 10_telemetry}/grafana/dashboards/htm-metrics.json +0 -0
  185. /data/examples/{rails_app → 12_rails_app}/.gitignore +0 -0
  186. /data/examples/{rails_app → 12_rails_app}/Procfile.dev +0 -0
  187. /data/examples/{rails_app → 12_rails_app}/README.md +0 -0
  188. /data/examples/{rails_app → 12_rails_app}/Rakefile +0 -0
  189. /data/examples/{rails_app → 12_rails_app}/app/assets/stylesheets/application.css +0 -0
  190. /data/examples/{rails_app → 12_rails_app}/app/assets/stylesheets/inter-font.css +0 -0
  191. /data/examples/{rails_app → 12_rails_app}/app/controllers/application_controller.rb +0 -0
  192. /data/examples/{rails_app → 12_rails_app}/app/controllers/search_controller.rb +0 -0
  193. /data/examples/{rails_app → 12_rails_app}/app/javascript/application.js +0 -0
  194. /data/examples/{rails_app → 12_rails_app}/app/javascript/controllers/application.js +0 -0
  195. /data/examples/{rails_app → 12_rails_app}/app/javascript/controllers/index.js +0 -0
  196. /data/examples/{rails_app → 12_rails_app}/app/views/files/index.html.erb +0 -0
  197. /data/examples/{rails_app → 12_rails_app}/app/views/files/show.html.erb +0 -0
  198. /data/examples/{rails_app → 12_rails_app}/app/views/layouts/application.html.erb +0 -0
  199. /data/examples/{rails_app → 12_rails_app}/app/views/memories/index.html.erb +0 -0
  200. /data/examples/{rails_app → 12_rails_app}/app/views/memories/new.html.erb +0 -0
  201. /data/examples/{rails_app → 12_rails_app}/app/views/robots/new.html.erb +0 -0
  202. /data/examples/{rails_app → 12_rails_app}/app/views/shared/_navbar.html.erb +0 -0
  203. /data/examples/{rails_app → 12_rails_app}/app/views/shared/_stat_card.html.erb +0 -0
  204. /data/examples/{rails_app → 12_rails_app}/bin/dev +0 -0
  205. /data/examples/{rails_app → 12_rails_app}/bin/rails +0 -0
  206. /data/examples/{rails_app → 12_rails_app}/bin/rake +0 -0
  207. /data/examples/{rails_app → 12_rails_app}/config/application.rb +0 -0
  208. /data/examples/{rails_app → 12_rails_app}/config/boot.rb +0 -0
  209. /data/examples/{rails_app → 12_rails_app}/config/database.yml +0 -0
  210. /data/examples/{rails_app → 12_rails_app}/config/environment.rb +0 -0
  211. /data/examples/{rails_app → 12_rails_app}/config/importmap.rb +0 -0
  212. /data/examples/{rails_app → 12_rails_app}/config/routes.rb +0 -0
  213. /data/examples/{rails_app → 12_rails_app}/config/tailwind.config.js +0 -0
  214. /data/examples/{rails_app → 12_rails_app}/config.ru +0 -0
  215. /data/examples/{rails_app → 12_rails_app}/log/.keep +0 -0
  216. /data/examples/{rails_app → 12_rails_app}/tmp/local_secret.txt +0 -0
@@ -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.current
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: "ActiveRecord not connected" } unless connected?
78
-
79
- pool = ActiveRecord::Base.connection_pool
80
-
81
- size = pool.size
82
- connections = pool.connections.size
83
- in_use = pool.connections.count(&:in_use?)
84
- available = connections - in_use
85
-
86
- # Calculate utilization based on connections in use vs pool size
87
- utilization = size > 0 ? in_use.to_f / size : 0.0
88
-
89
- # Determine health status
90
- status = case
91
- when available == 0 && in_use >= size
92
- :exhausted
93
- when utilization >= POOL_CRITICAL_THRESHOLD
94
- :critical
95
- when utilization >= POOL_WARNING_THRESHOLD
96
- :warning
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: size,
103
- connections: connections,
104
- in_use: in_use,
105
- available: available,
106
- utilization: (utilization * 100).round(2),
107
- status: status,
108
- wait_timeout: pool.checkout_timeout * 1000 # Convert to ms
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.current
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.current
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.current
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.current
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 ActiveRecord is connected
320
+ # Check if Sequel database is connected
317
321
  def connected?
318
- return false unless defined?(ActiveRecord::Base)
319
- ActiveRecord::Base.connected? && ActiveRecord::Base.connection.active?
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 = ActiveRecord::Base.connection.select_value(
327
- ActiveRecord::Base.sanitize_sql_array(
328
- ["SELECT COUNT(*) FROM pg_extension WHERE extname = ?", name]
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
- MIN_PROPOSITION_LENGTH = 10 # Minimum characters for a valid proposition
30
- MAX_PROPOSITION_LENGTH = 1000 # Maximum characters for a valid proposition
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 < MIN_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 > MAX_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 < MIN_PROPOSITION_LENGTH
153
- return false if proposition.length > MAX_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::ActiveRecordConfig.establish_connection! unless HTM::ActiveRecordConfig.connected?
61
- HTM::ActiveRecordConfig.verify_extensions!
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}"
@@ -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
- .update_all(working_memory: false)
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 [ActiveRecord::Relation<HTM::Models::Node>] Collection of nodes
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
- .pluck(:node_id)
381
+ .select_map(:node_id)
382
382
 
383
- HTM::Models::Node.where(id: node_ids).order(created_at: :desc)
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
- .update_all(working_memory: false)
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
- .where.not(robot_id: htm.robot_id)
438
+ .exclude(robot_id: htm.robot_id)
439
439
  .distinct
440
- .pluck(:node_id)
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.find_or_initialize_by(
445
+ robot_node = HTM::Models::RobotNode.first(
446
446
  robot_id: htm.robot_id,
447
447
  node_id: node_id
448
448
  )
449
- next if robot_node.working_memory?
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
- .pluck(:node_id)
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
- .pluck(:node_id)
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.find_or_initialize_by(
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
- .update_all(working_memory: false)
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.find_by(id: node_id)
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.find_or_initialize_by(
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