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/observability.rb
CHANGED
|
@@ -93,14 +93,14 @@ class HTM
|
|
|
93
93
|
# - Pool is healthy if no threads are waiting
|
|
94
94
|
# - Pool is critical only if threads are waiting for connections
|
|
95
95
|
# - size == max_size is normal (pre-allocated pool), not a problem
|
|
96
|
-
status = if waiting
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
status = if waiting.positive?
|
|
97
|
+
waiting > max_size / 2 ? :exhausted : :critical
|
|
98
|
+
else
|
|
99
|
+
:healthy
|
|
100
|
+
end
|
|
101
101
|
|
|
102
102
|
# Utilization based on waiting threads (pool stress indicator)
|
|
103
|
-
utilization = waiting
|
|
103
|
+
utilization = waiting.positive? ? ((waiting.to_f / max_size) * 100).round(2) : 0.0
|
|
104
104
|
|
|
105
105
|
stats = {
|
|
106
106
|
size: max_size,
|
|
@@ -252,47 +252,11 @@ class HTM
|
|
|
252
252
|
def health_check
|
|
253
253
|
checks = {}
|
|
254
254
|
issues = []
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
checks
|
|
258
|
-
issues
|
|
259
|
-
|
|
260
|
-
# Check connection pool
|
|
261
|
-
pool_stats = connection_pool_stats
|
|
262
|
-
checks[:connection_pool] = pool_stats[:status] == :healthy || pool_stats[:status] == :warning
|
|
263
|
-
issues << "Connection pool #{pool_stats[:status]}" if [:critical, :exhausted].include?(pool_stats[:status])
|
|
264
|
-
|
|
265
|
-
# Check circuit breakers
|
|
266
|
-
cb_stats = circuit_breaker_stats
|
|
267
|
-
if cb_stats[:embedding_service]
|
|
268
|
-
checks[:embedding_circuit] = cb_stats[:embedding_service][:state] != :open
|
|
269
|
-
issues << "Embedding service circuit breaker open" unless checks[:embedding_circuit]
|
|
270
|
-
end
|
|
271
|
-
if cb_stats[:tag_service]
|
|
272
|
-
checks[:tag_circuit] = cb_stats[:tag_service][:state] != :open
|
|
273
|
-
issues << "Tag service circuit breaker open" unless checks[:tag_circuit]
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
# Check required extensions
|
|
277
|
-
if connected?
|
|
278
|
-
begin
|
|
279
|
-
checks[:pgvector] = extension_installed?('vector')
|
|
280
|
-
issues << "pgvector extension not installed" unless checks[:pgvector]
|
|
281
|
-
|
|
282
|
-
checks[:pg_trgm] = extension_installed?('pg_trgm')
|
|
283
|
-
issues << "pg_trgm extension not installed" unless checks[:pg_trgm]
|
|
284
|
-
rescue StandardError => e
|
|
285
|
-
checks[:extensions] = false
|
|
286
|
-
issues << "Failed to check extensions: #{e.message}"
|
|
287
|
-
end
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
{
|
|
291
|
-
healthy: issues.empty?,
|
|
292
|
-
checks: checks,
|
|
293
|
-
issues: issues,
|
|
294
|
-
checked_at: Time.now
|
|
295
|
-
}
|
|
255
|
+
check_database(checks, issues)
|
|
256
|
+
check_pool(checks, issues)
|
|
257
|
+
check_circuit_breakers(checks, issues)
|
|
258
|
+
check_extensions(checks, issues) if connected?
|
|
259
|
+
{ healthy: issues.empty?, checks: checks, issues: issues, checked_at: Time.now }
|
|
296
260
|
end
|
|
297
261
|
|
|
298
262
|
# Quick health check - returns boolean
|
|
@@ -317,6 +281,38 @@ class HTM
|
|
|
317
281
|
|
|
318
282
|
private
|
|
319
283
|
|
|
284
|
+
def check_database(checks, issues)
|
|
285
|
+
checks[:database] = connected?
|
|
286
|
+
issues << "Database not connected" unless checks[:database]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def check_pool(checks, issues)
|
|
290
|
+
pool_stats = connection_pool_stats
|
|
291
|
+
checks[:connection_pool] = %i[healthy warning].include?(pool_stats[:status])
|
|
292
|
+
issues << "Connection pool #{pool_stats[:status]}" if %i[critical exhausted].include?(pool_stats[:status])
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def check_circuit_breakers(checks, issues)
|
|
296
|
+
cb_stats = circuit_breaker_stats
|
|
297
|
+
if cb_stats[:embedding_service]
|
|
298
|
+
checks[:embedding_circuit] = cb_stats[:embedding_service][:state] != :open
|
|
299
|
+
issues << "Embedding service circuit breaker open" unless checks[:embedding_circuit]
|
|
300
|
+
end
|
|
301
|
+
return unless cb_stats[:tag_service]
|
|
302
|
+
checks[:tag_circuit] = cb_stats[:tag_service][:state] != :open
|
|
303
|
+
issues << "Tag service circuit breaker open" unless checks[:tag_circuit]
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def check_extensions(checks, issues)
|
|
307
|
+
checks[:pgvector] = extension_installed?('vector')
|
|
308
|
+
issues << "pgvector extension not installed" unless checks[:pgvector]
|
|
309
|
+
checks[:pg_trgm] = extension_installed?('pg_trgm')
|
|
310
|
+
issues << "pg_trgm extension not installed" unless checks[:pg_trgm]
|
|
311
|
+
rescue StandardError => e
|
|
312
|
+
checks[:extensions] = false
|
|
313
|
+
issues << "Failed to check extensions: #{e.message}"
|
|
314
|
+
end
|
|
315
|
+
|
|
320
316
|
# Check if Sequel database is connected
|
|
321
317
|
def connected?
|
|
322
318
|
return false unless defined?(HTM) && HTM.respond_to?(:db)
|
|
@@ -332,7 +328,7 @@ class HTM
|
|
|
332
328
|
result = HTM.db.fetch(
|
|
333
329
|
"SELECT COUNT(*) AS cnt FROM pg_extension WHERE extname = ?", name
|
|
334
330
|
).first
|
|
335
|
-
result[:cnt].to_i
|
|
331
|
+
result[:cnt].to_i.positive?
|
|
336
332
|
end
|
|
337
333
|
|
|
338
334
|
# Calculate timing statistics from samples
|
|
@@ -365,7 +361,7 @@ class HTM
|
|
|
365
361
|
|
|
366
362
|
return sorted_array[f] if f == c
|
|
367
363
|
|
|
368
|
-
sorted_array[f] * (c - k) + sorted_array[c] * (k - f)
|
|
364
|
+
(sorted_array[f] * (c - k)) + (sorted_array[c] * (k - f))
|
|
369
365
|
end
|
|
370
366
|
|
|
371
367
|
# Get process memory in MB
|
|
@@ -376,8 +372,6 @@ class HTM
|
|
|
376
372
|
elsif File.exist?('/proc/self/status')
|
|
377
373
|
# Linux: Read from proc
|
|
378
374
|
File.read('/proc/self/status').match(/VmRSS:\s+(\d+)/)[1].to_i / 1024.0
|
|
379
|
-
else
|
|
380
|
-
nil
|
|
381
375
|
end
|
|
382
376
|
rescue StandardError
|
|
383
377
|
nil
|
|
@@ -89,11 +89,7 @@ class HTM
|
|
|
89
89
|
|
|
90
90
|
# Validate and filter propositions
|
|
91
91
|
validate_and_filter_propositions(parsed_propositions)
|
|
92
|
-
|
|
93
|
-
rescue HTM::CircuitBreakerOpenError
|
|
94
|
-
# Re-raise circuit breaker errors without wrapping
|
|
95
|
-
raise
|
|
96
|
-
rescue HTM::PropositionError
|
|
92
|
+
rescue HTM::CircuitBreakerOpenError, HTM::PropositionError
|
|
97
93
|
raise
|
|
98
94
|
rescue StandardError => e
|
|
99
95
|
HTM.logger.error "PropositionService: Failed to extract propositions: #{e.message}"
|
|
@@ -149,9 +145,9 @@ class HTM
|
|
|
149
145
|
# @return [Integer] Minimum word count for valid propositions
|
|
150
146
|
#
|
|
151
147
|
def self.min_words
|
|
152
|
-
HTM.config.proposition.min_words ||
|
|
148
|
+
HTM.config.proposition.min_words || 3
|
|
153
149
|
rescue
|
|
154
|
-
|
|
150
|
+
3
|
|
155
151
|
end
|
|
156
152
|
|
|
157
153
|
# Check if proposition is a meta-response (LLM asking for input)
|
data/lib/htm/query_cache.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'lru_redux'
|
|
4
|
-
require 'set'
|
|
5
4
|
|
|
6
5
|
class HTM
|
|
7
6
|
# Thread-safe query result cache with TTL and statistics
|
|
@@ -31,7 +30,7 @@ class HTM
|
|
|
31
30
|
attr_reader :enabled
|
|
32
31
|
|
|
33
32
|
# Cache key prefix for method-based invalidation
|
|
34
|
-
METHOD_PREFIX = "m:"
|
|
33
|
+
METHOD_PREFIX = "m:"
|
|
35
34
|
|
|
36
35
|
# Initialize a new query cache
|
|
37
36
|
#
|
|
@@ -39,16 +38,15 @@ class HTM
|
|
|
39
38
|
# @param ttl [Integer] Time-to-live in seconds (default: 300)
|
|
40
39
|
#
|
|
41
40
|
def initialize(size: 1000, ttl: 300)
|
|
42
|
-
@enabled = size
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
end
|
|
41
|
+
@enabled = size.positive?
|
|
42
|
+
|
|
43
|
+
return unless @enabled
|
|
44
|
+
@cache = LruRedux::TTL::ThreadSafeCache.new(size, ttl)
|
|
45
|
+
@hits = 0
|
|
46
|
+
@misses = 0
|
|
47
|
+
@mutex = Mutex.new
|
|
48
|
+
# Track keys by method for selective invalidation
|
|
49
|
+
@keys_by_method = Hash.new { |h, k| h[k] = Set.new }
|
|
52
50
|
end
|
|
53
51
|
|
|
54
52
|
# Fetch a value from cache or execute block
|
|
@@ -58,10 +56,10 @@ class HTM
|
|
|
58
56
|
# @yield Block that computes the value if not cached
|
|
59
57
|
# @return [Object] Cached or computed value
|
|
60
58
|
#
|
|
61
|
-
def fetch(method,
|
|
59
|
+
def fetch(method, *, &)
|
|
62
60
|
return yield unless @enabled
|
|
63
61
|
|
|
64
|
-
key = cache_key(method, *
|
|
62
|
+
key = cache_key(method, *)
|
|
65
63
|
|
|
66
64
|
if (cached = @cache[key])
|
|
67
65
|
@mutex.synchronize { @hits += 1 }
|
|
@@ -131,7 +129,7 @@ class HTM
|
|
|
131
129
|
return nil unless @enabled
|
|
132
130
|
|
|
133
131
|
total = @hits + @misses
|
|
134
|
-
hit_rate = total
|
|
132
|
+
hit_rate = total.positive? ? (@hits.to_f / total * 100).round(2) : 0.0
|
|
135
133
|
|
|
136
134
|
{
|
|
137
135
|
hits: @hits,
|
data/lib/htm/railtie.rb
CHANGED
|
@@ -26,7 +26,7 @@ class HTM
|
|
|
26
26
|
railtie_name :htm
|
|
27
27
|
|
|
28
28
|
# Configure HTM before Rails initializers run
|
|
29
|
-
initializer "htm.configure" do |
|
|
29
|
+
initializer "htm.configure" do |_app|
|
|
30
30
|
HTM.configure do |config|
|
|
31
31
|
# Use Rails logger
|
|
32
32
|
config.logger = Rails.logger
|
|
@@ -66,6 +66,5 @@ class HTM
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
|
-
|
|
70
69
|
end
|
|
71
70
|
end
|
data/lib/htm/robot_group.rb
CHANGED
|
@@ -320,7 +320,7 @@ class HTM
|
|
|
320
320
|
# originator: 'agent-2'
|
|
321
321
|
# )
|
|
322
322
|
#
|
|
323
|
-
def remember(content, originator: nil, **
|
|
323
|
+
def remember(content, originator: nil, **)
|
|
324
324
|
raise 'No active robots in group' if @active_robots.empty?
|
|
325
325
|
|
|
326
326
|
# Use first active robot (or specified originator) to create the memory
|
|
@@ -330,7 +330,7 @@ class HTM
|
|
|
330
330
|
@active_robots.values.first
|
|
331
331
|
end
|
|
332
332
|
|
|
333
|
-
node_id = primary.remember(content, **
|
|
333
|
+
node_id = primary.remember(content, **)
|
|
334
334
|
|
|
335
335
|
# Sync to database (robot_nodes table) for all other members
|
|
336
336
|
sync_node_to_members(node_id, exclude: primary.robot_id)
|
|
@@ -356,11 +356,11 @@ class HTM
|
|
|
356
356
|
# @example
|
|
357
357
|
# results = group.recall('billing issue', limit: 5, strategy: :fulltext)
|
|
358
358
|
#
|
|
359
|
-
def recall(query, **
|
|
359
|
+
def recall(query, **)
|
|
360
360
|
raise 'No active robots in group' if @active_robots.empty?
|
|
361
361
|
|
|
362
362
|
primary = @active_robots.values.first
|
|
363
|
-
primary.recall(query, **
|
|
363
|
+
primary.recall(query, **)
|
|
364
364
|
end
|
|
365
365
|
|
|
366
366
|
# Returns all nodes currently in shared working memory.
|
|
@@ -476,7 +476,7 @@ class HTM
|
|
|
476
476
|
|
|
477
477
|
all_robots.each_key do |robot_name|
|
|
478
478
|
synced = sync_robot(robot_name)
|
|
479
|
-
if synced
|
|
479
|
+
if synced.positive?
|
|
480
480
|
members_updated += 1
|
|
481
481
|
total_synced += synced
|
|
482
482
|
end
|
|
@@ -634,7 +634,7 @@ class HTM
|
|
|
634
634
|
working_memory_nodes: wm_contents.count,
|
|
635
635
|
working_memory_tokens: token_count,
|
|
636
636
|
max_tokens: @max_tokens,
|
|
637
|
-
token_utilization: @max_tokens
|
|
637
|
+
token_utilization: @max_tokens.positive? ? (token_count.to_f / @max_tokens).round(2) : 0,
|
|
638
638
|
in_sync: in_sync?
|
|
639
639
|
}
|
|
640
640
|
end
|
|
@@ -685,7 +685,7 @@ class HTM
|
|
|
685
685
|
node = HTM::Models::Node.first(id: node_id)
|
|
686
686
|
return unless node
|
|
687
687
|
|
|
688
|
-
all_robots.
|
|
688
|
+
all_robots.each_value do |htm|
|
|
689
689
|
next if htm.robot_id == origin_robot_id
|
|
690
690
|
|
|
691
691
|
htm.working_memory.add_from_sync(
|
|
@@ -698,7 +698,7 @@ class HTM
|
|
|
698
698
|
end
|
|
699
699
|
|
|
700
700
|
def evict_from_in_memory_caches(node_id, origin_robot_id)
|
|
701
|
-
all_robots.
|
|
701
|
+
all_robots.each_value do |htm|
|
|
702
702
|
next if htm.robot_id == origin_robot_id
|
|
703
703
|
|
|
704
704
|
htm.working_memory.remove_from_sync(node_id)
|
|
@@ -706,7 +706,7 @@ class HTM
|
|
|
706
706
|
end
|
|
707
707
|
|
|
708
708
|
def clear_all_in_memory_caches(origin_robot_id)
|
|
709
|
-
all_robots.
|
|
709
|
+
all_robots.each_value do |htm|
|
|
710
710
|
next if htm.robot_id == origin_robot_id
|
|
711
711
|
|
|
712
712
|
htm.working_memory.clear_from_sync
|
data/lib/htm/sequel_config.rb
CHANGED
data/lib/htm/sql_builder.rb
CHANGED
data/lib/htm/tag_service.rb
CHANGED
|
@@ -16,7 +16,7 @@ class HTM
|
|
|
16
16
|
# The actual LLM call is delegated to HTM.configuration.tag_extractor
|
|
17
17
|
#
|
|
18
18
|
class TagService
|
|
19
|
-
TAG_FORMAT = /^[a-z0-9
|
|
19
|
+
TAG_FORMAT = /^[a-z0-9-]+(:[a-z0-9-]+)*$/ # Validation regex
|
|
20
20
|
|
|
21
21
|
# Circuit breaker for tag extraction API calls
|
|
22
22
|
@circuit_breaker = nil
|
|
@@ -76,11 +76,7 @@ class HTM
|
|
|
76
76
|
|
|
77
77
|
# Validate and filter tags
|
|
78
78
|
validate_and_filter_tags(parsed_tags)
|
|
79
|
-
|
|
80
|
-
rescue HTM::CircuitBreakerOpenError
|
|
81
|
-
# Re-raise circuit breaker errors without wrapping
|
|
82
|
-
raise
|
|
83
|
-
rescue HTM::TagError
|
|
79
|
+
rescue HTM::CircuitBreakerOpenError, HTM::TagError
|
|
84
80
|
raise
|
|
85
81
|
rescue StandardError => e
|
|
86
82
|
HTM.logger.error "TagService: Failed to extract tags: #{e.message}"
|
data/lib/htm/timeframe.rb
CHANGED
|
@@ -61,7 +61,7 @@ class HTM
|
|
|
61
61
|
|
|
62
62
|
else
|
|
63
63
|
raise ArgumentError, "Unsupported timeframe type: #{input.class}. " \
|
|
64
|
-
|
|
64
|
+
"Expected nil, Range, Array<Range>, Date, DateTime, Time, String, or :auto"
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
@@ -75,7 +75,7 @@ class HTM
|
|
|
75
75
|
when nil, :auto, Range, Date, DateTime, Time, String
|
|
76
76
|
true
|
|
77
77
|
when Array
|
|
78
|
-
input.all?
|
|
78
|
+
input.all?(Range)
|
|
79
79
|
else
|
|
80
80
|
false
|
|
81
81
|
end
|
|
@@ -185,9 +185,8 @@ class HTM
|
|
|
185
185
|
# @raise [ArgumentError] If range is invalid
|
|
186
186
|
#
|
|
187
187
|
def validate_range!(range)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
end
|
|
188
|
+
return if range.begin.respond_to?(:to_time) && range.end.respond_to?(:to_time)
|
|
189
|
+
raise ArgumentError, "Range must have Time-compatible begin and end values"
|
|
191
190
|
end
|
|
192
191
|
end
|
|
193
192
|
end
|
|
@@ -44,6 +44,17 @@ class HTM
|
|
|
44
44
|
'six' => 6, 'seven' => 7, 'eight' => 8, 'nine' => 9, 'ten' => 10
|
|
45
45
|
}.freeze
|
|
46
46
|
|
|
47
|
+
# Seconds per singular time unit (used by parse_last_x and parse_recent)
|
|
48
|
+
UNIT_SECONDS = {
|
|
49
|
+
'second' => 1,
|
|
50
|
+
'minute' => 60,
|
|
51
|
+
'hour' => 3_600,
|
|
52
|
+
'day' => 86_400,
|
|
53
|
+
'week' => 604_800,
|
|
54
|
+
'month' => 2_592_000,
|
|
55
|
+
'year' => 31_536_000
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
47
58
|
# Patterns for temporal expressions (order matters - more specific first)
|
|
48
59
|
# Each pattern should match ORIGINAL text (including "few", "a few")
|
|
49
60
|
TEMPORAL_PATTERNS = [
|
|
@@ -84,7 +95,7 @@ class HTM
|
|
|
84
95
|
/\b(?:recently|recent)\b/i,
|
|
85
96
|
|
|
86
97
|
# Standard time words
|
|
87
|
-
/\b(?:yesterday|today|tonight|this\s+morning|this\s+afternoon|this\s+evening|last\s+night)\b/i
|
|
98
|
+
/\b(?:yesterday|today|tonight|this\s+morning|this\s+afternoon|this\s+evening|last\s+night)\b/i
|
|
88
99
|
].freeze
|
|
89
100
|
|
|
90
101
|
# Result structure for extracted timeframe
|
|
@@ -153,49 +164,21 @@ class HTM
|
|
|
153
164
|
# @return [Time, Range, nil] Parsed timeframe
|
|
154
165
|
#
|
|
155
166
|
def parse_expression(expression)
|
|
156
|
-
|
|
157
|
-
if expression.match?(/\
|
|
158
|
-
return parse_recent
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Handle "weekend before last" - 2 weekends ago
|
|
162
|
-
if expression.match?(/\bweekend\s+before\s+last\b/i)
|
|
163
|
-
return parse_weekends_ago(2)
|
|
164
|
-
end
|
|
167
|
+
return parse_recent if expression.match?(/\b(?:recently|recent)\b/i)
|
|
168
|
+
return parse_weekends_ago(2) if expression.match?(/\bweekend\s+before\s+last\b/i)
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
count = parse_number(match[1])
|
|
169
|
-
return parse_weekends_ago(count)
|
|
170
|
+
if (match = expression.match(/\b(\d+|one|two|three|four|five|six|seven|eight|nine|ten|few|a\s+few|several)\s+weekends?\s+ago\b/i))
|
|
171
|
+
return parse_weekends_ago(parse_number(match[1]))
|
|
170
172
|
end
|
|
171
173
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
normalized = normalize_few_keywords(expression)
|
|
175
|
+
chronic_expr = normalized.gsub(/\bin\s+the\s+/i, '')
|
|
174
176
|
|
|
175
|
-
|
|
176
|
-
if match = normalized.match(/(?:in\s+the\s+)?(?:last|past)\s+(\d+)\s+(#{TIME_UNITS})/i)
|
|
177
|
+
if (match = normalized.match(/(?:in\s+the\s+)?(?:last|past)\s+(\d+)\s+(#{TIME_UNITS})/i))
|
|
177
178
|
return parse_last_x(match[1].to_i, match[2])
|
|
178
179
|
end
|
|
179
180
|
|
|
180
|
-
|
|
181
|
-
chronic_expr = normalized.gsub(/\bin\s+the\s+/i, '')
|
|
182
|
-
|
|
183
|
-
# Get week_start from HTM configuration (default: :sunday)
|
|
184
|
-
week_start = :sunday
|
|
185
|
-
if defined?(HTM) && HTM.respond_to?(:configuration)
|
|
186
|
-
week_start = HTM.configuration.week_start
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# Try to get a span/range first
|
|
190
|
-
result = Chronic.parse(chronic_expr, guess: false, week_start: week_start)
|
|
191
|
-
|
|
192
|
-
# Convert Chronic::Span to Range if needed
|
|
193
|
-
if result.respond_to?(:begin) && result.respond_to?(:end)
|
|
194
|
-
return result.begin..result.end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Fall back to point in time
|
|
198
|
-
Chronic.parse(chronic_expr, week_start: week_start)
|
|
181
|
+
chronic_parse_expression(chronic_expr)
|
|
199
182
|
end
|
|
200
183
|
|
|
201
184
|
# Parse a number from string (numeric or written word)
|
|
@@ -205,7 +188,7 @@ class HTM
|
|
|
205
188
|
#
|
|
206
189
|
def parse_number(str)
|
|
207
190
|
normalized = str.downcase.strip
|
|
208
|
-
return FEW if
|
|
191
|
+
return FEW if ['few', 'a few', 'several'].include?(normalized)
|
|
209
192
|
return WORD_NUMBERS[normalized] if WORD_NUMBERS.key?(normalized)
|
|
210
193
|
normalized.to_i
|
|
211
194
|
end
|
|
@@ -217,23 +200,26 @@ class HTM
|
|
|
217
200
|
#
|
|
218
201
|
def parse_weekends_ago(count)
|
|
219
202
|
now = Time.now
|
|
203
|
+
target_saturday = last_saturday_before(now) - ((count - 1) * 7 * 86_400)
|
|
204
|
+
target_saturday..(target_saturday + (2 * 86_400))
|
|
205
|
+
end
|
|
220
206
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
# Go back (count - 1) more weeks to get to the target weekend
|
|
228
|
-
# count=1 means "last weekend" = the most recent past weekend
|
|
229
|
-
# count=2 means "weekend before last" = 2 weekends ago
|
|
230
|
-
target_saturday = last_saturday - ((count - 1) * 7 * 24 * 60 * 60)
|
|
207
|
+
def chronic_parse_expression(expr)
|
|
208
|
+
week_start = fetch_week_start
|
|
209
|
+
result = Chronic.parse(expr, guess: false, week_start: week_start)
|
|
210
|
+
return result.begin..result.end if result.respond_to?(:begin) && result.respond_to?(:end)
|
|
211
|
+
Chronic.parse(expr, week_start: week_start)
|
|
212
|
+
end
|
|
231
213
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
214
|
+
def fetch_week_start
|
|
215
|
+
return HTM.configuration.week_start if defined?(HTM) && HTM.respond_to?(:configuration)
|
|
216
|
+
:sunday
|
|
217
|
+
end
|
|
235
218
|
|
|
236
|
-
|
|
219
|
+
def last_saturday_before(now)
|
|
220
|
+
days_since = (now.wday - 6) % 7
|
|
221
|
+
days_since = 7 if days_since.zero? && now.wday != 6
|
|
222
|
+
Time.new(now.year, now.month, now.day, 0, 0, 0) - (days_since * 86_400)
|
|
237
223
|
end
|
|
238
224
|
|
|
239
225
|
# Parse "last X units" or "past X units" to a proper range
|
|
@@ -244,19 +230,8 @@ class HTM
|
|
|
244
230
|
#
|
|
245
231
|
def parse_last_x(count, unit)
|
|
246
232
|
now = Time.now
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
seconds = case unit_normalized
|
|
250
|
-
when 'second' then count
|
|
251
|
-
when 'minute' then count * 60
|
|
252
|
-
when 'hour' then count * 60 * 60
|
|
253
|
-
when 'day' then count * 24 * 60 * 60
|
|
254
|
-
when 'week' then count * 7 * 24 * 60 * 60
|
|
255
|
-
when 'month' then count * 30 * 24 * 60 * 60
|
|
256
|
-
when 'year' then count * 365 * 24 * 60 * 60
|
|
257
|
-
else count * 24 * 60 * 60 # Default to days
|
|
258
|
-
end
|
|
259
|
-
|
|
233
|
+
unit_singular = unit.downcase.sub(/s$/, '')
|
|
234
|
+
seconds = count * (UNIT_SECONDS[unit_singular] || UNIT_SECONDS['day'])
|
|
260
235
|
(now - seconds)..now
|
|
261
236
|
end
|
|
262
237
|
|
|
@@ -266,24 +241,8 @@ class HTM
|
|
|
266
241
|
#
|
|
267
242
|
def parse_recent
|
|
268
243
|
now = Time.now
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
(now - FEW)..now
|
|
272
|
-
when :minutes
|
|
273
|
-
(now - (FEW * 60))..now
|
|
274
|
-
when :hours
|
|
275
|
-
(now - (FEW * 60 * 60))..now
|
|
276
|
-
when :days
|
|
277
|
-
(now - (FEW * 24 * 60 * 60))..now
|
|
278
|
-
when :weeks
|
|
279
|
-
(now - (FEW * 7 * 24 * 60 * 60))..now
|
|
280
|
-
when :months
|
|
281
|
-
(now - (FEW * 30 * 24 * 60 * 60))..now
|
|
282
|
-
when :years
|
|
283
|
-
(now - (FEW * 365 * 24 * 60 * 60))..now
|
|
284
|
-
else
|
|
285
|
-
(now - (FEW * 24 * 60 * 60))..now
|
|
286
|
-
end
|
|
244
|
+
seconds = FEW * (UNIT_SECONDS[DEFAULT_RECENT_UNIT.to_s] || UNIT_SECONDS['day'])
|
|
245
|
+
(now - seconds)..now
|
|
287
246
|
end
|
|
288
247
|
|
|
289
248
|
# Clean the query by removing the temporal expression
|
data/lib/htm/version.rb
CHANGED