htm 0.0.30 → 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/defaults.yml +25 -13
- data/lib/htm/config/validator.rb +12 -18
- data/lib/htm/config.rb +93 -173
- 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 +28 -106
- data/lib/htm/config/section.rb +0 -74
- data/lib/htm/loaders/defaults_loader.rb +0 -166
- data/lib/htm/loaders/xdg_config_loader.rb +0 -116
data/lib/htm/mcp/cli.rb
CHANGED
|
@@ -138,51 +138,16 @@ class HTM
|
|
|
138
138
|
end
|
|
139
139
|
|
|
140
140
|
def check_database_config!
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
end
|
|
141
|
+
return if ENV['HTM_DATABASE__URL'] || ENV['HTM_DATABASE__NAME']
|
|
142
|
+
warn "Error: Database not configured."
|
|
143
|
+
warn "Set HTM_DATABASE__URL or HTM_DATABASE__NAME environment variable."
|
|
144
|
+
warn "Run 'htm_mcp help' for details."
|
|
145
|
+
exit 1
|
|
147
146
|
end
|
|
148
147
|
|
|
149
148
|
def print_error_suggestion(error_message)
|
|
150
|
-
msg = error_message.to_s.downcase
|
|
151
|
-
|
|
152
149
|
warn ""
|
|
153
|
-
|
|
154
|
-
warn "Suggestion: The database does not exist. Create it with:"
|
|
155
|
-
warn " createdb #{extract_dbname(ENV['HTM_DATABASE__URL'] || ENV['HTM_DATABASE__NAME'])}"
|
|
156
|
-
warn "Then initialize the schema with:"
|
|
157
|
-
warn " htm_mcp setup"
|
|
158
|
-
elsif msg.include?("password authentication failed") || msg.include?("no password supplied")
|
|
159
|
-
warn "Suggestion: Check your database credentials."
|
|
160
|
-
warn "Verify HTM_DATABASE__URL has correct username and password:"
|
|
161
|
-
warn " postgresql://USER:PASSWORD@localhost:5432/DATABASE"
|
|
162
|
-
elsif msg.include?("connection refused") || msg.include?("could not connect")
|
|
163
|
-
warn "Suggestion: PostgreSQL server is not running or not accepting connections."
|
|
164
|
-
warn "Start PostgreSQL with:"
|
|
165
|
-
warn " brew services start postgresql@17 # macOS with Homebrew"
|
|
166
|
-
warn " sudo systemctl start postgresql # Linux"
|
|
167
|
-
elsif msg.include?("role") && msg.include?("does not exist")
|
|
168
|
-
warn "Suggestion: The database user does not exist. Create it with:"
|
|
169
|
-
warn " createuser -s YOUR_USERNAME"
|
|
170
|
-
elsif msg.include?("permission denied")
|
|
171
|
-
warn "Suggestion: The user lacks permission to access this database."
|
|
172
|
-
warn "Grant access or use a different user with appropriate privileges."
|
|
173
|
-
elsif msg.include?("timeout") || msg.include?("timed out")
|
|
174
|
-
warn "Suggestion: Connection timed out. Check:"
|
|
175
|
-
warn " - PostgreSQL is running"
|
|
176
|
-
warn " - Firewall allows connections on port 5432"
|
|
177
|
-
warn " - Host address is correct"
|
|
178
|
-
elsif msg.include?("extension") && msg.include?("vector")
|
|
179
|
-
warn "Suggestion: pgvector extension is not installed. Install it with:"
|
|
180
|
-
warn " brew install pgvector # macOS"
|
|
181
|
-
warn "Then enable it in your database:"
|
|
182
|
-
warn " psql -d DATABASE -c 'CREATE EXTENSION vector;'"
|
|
183
|
-
else
|
|
184
|
-
warn "Suggestion: Run 'htm_mcp help' for configuration details."
|
|
185
|
-
end
|
|
150
|
+
suggestion_lines_for(error_message.to_s.downcase).each { |line| warn line }
|
|
186
151
|
end
|
|
187
152
|
|
|
188
153
|
def extract_dbname(url_or_name)
|
|
@@ -190,7 +155,7 @@ class HTM
|
|
|
190
155
|
|
|
191
156
|
# Extract database name from URL like postgresql://user@host:port/dbname
|
|
192
157
|
if url_or_name =~ %r{/([^/?]+)(?:\?|$)}
|
|
193
|
-
|
|
158
|
+
::Regexp.last_match(1)
|
|
194
159
|
else
|
|
195
160
|
"htm_development"
|
|
196
161
|
end
|
|
@@ -231,7 +196,7 @@ class HTM
|
|
|
231
196
|
pending = check_migration_status
|
|
232
197
|
puts
|
|
233
198
|
|
|
234
|
-
if pending
|
|
199
|
+
if pending.positive?
|
|
235
200
|
warn "Warning: #{pending} pending migration(s) detected."
|
|
236
201
|
warn " Run 'htm_mcp setup' to apply pending migrations."
|
|
237
202
|
puts
|
|
@@ -255,7 +220,8 @@ class HTM
|
|
|
255
220
|
version: File.basename(file).split('_').first,
|
|
256
221
|
name: File.basename(file, '.rb')
|
|
257
222
|
}
|
|
258
|
-
end
|
|
223
|
+
end
|
|
224
|
+
available_migrations = available_migrations.sort_by { |m| m[:version] }
|
|
259
225
|
|
|
260
226
|
# Ensure Sequel connection for migration check
|
|
261
227
|
HTM::SequelConfig.establish_connection!
|
|
@@ -308,12 +274,9 @@ class HTM
|
|
|
308
274
|
|
|
309
275
|
begin
|
|
310
276
|
require 'yaml'
|
|
311
|
-
config_data = YAML.
|
|
312
|
-
File.read(path),
|
|
313
|
-
permitted_classes: [Symbol],
|
|
277
|
+
config_data = YAML.safe_load_file(path, permitted_classes: [Symbol],
|
|
314
278
|
symbolize_names: true,
|
|
315
|
-
aliases: true
|
|
316
|
-
) || {}
|
|
279
|
+
aliases: true) || {}
|
|
317
280
|
|
|
318
281
|
# Determine which section to use based on environment
|
|
319
282
|
env = HTM::Config.env.to_sym
|
|
@@ -425,7 +388,7 @@ class HTM
|
|
|
425
388
|
args = args.dup
|
|
426
389
|
|
|
427
390
|
# Handle -c / --config option first (can be combined with other commands)
|
|
428
|
-
|
|
391
|
+
handle_config_option(args)
|
|
429
392
|
|
|
430
393
|
# Process remaining command
|
|
431
394
|
case args[0]&.downcase
|
|
@@ -448,12 +411,12 @@ class HTM
|
|
|
448
411
|
# 'stdio' is accepted for compatibility with MCP clients that pass it as an argument
|
|
449
412
|
return false
|
|
450
413
|
when /^-/
|
|
451
|
-
|
|
452
|
-
|
|
414
|
+
warn "Unknown option: #{args[0]}"
|
|
415
|
+
warn "Run 'htm_mcp help' for usage."
|
|
453
416
|
exit 1
|
|
454
417
|
else
|
|
455
|
-
|
|
456
|
-
|
|
418
|
+
warn "Unknown command: #{args[0]}"
|
|
419
|
+
warn "Run 'htm_mcp help' for usage."
|
|
457
420
|
exit 1
|
|
458
421
|
end
|
|
459
422
|
true
|
|
@@ -494,7 +457,7 @@ class HTM
|
|
|
494
457
|
if args.empty? || args.first == '--tasks' || args.first == '-T'
|
|
495
458
|
# Check for optional pattern after -T/--tasks
|
|
496
459
|
pattern = nil
|
|
497
|
-
if
|
|
460
|
+
if ['--tasks', '-T'].include?(args.first)
|
|
498
461
|
pattern = args[1] # May be nil if no pattern provided
|
|
499
462
|
end
|
|
500
463
|
list_rake_tasks(pattern: pattern)
|
|
@@ -532,7 +495,7 @@ class HTM
|
|
|
532
495
|
|
|
533
496
|
# Load all HTM task files
|
|
534
497
|
tasks_dir = File.expand_path('../../tasks', __dir__)
|
|
535
|
-
Dir.glob(File.join(tasks_dir, '*.rake')).
|
|
498
|
+
Dir.glob(File.join(tasks_dir, '*.rake')).each do |rake_file|
|
|
536
499
|
load rake_file
|
|
537
500
|
end
|
|
538
501
|
end
|
|
@@ -542,8 +505,8 @@ class HTM
|
|
|
542
505
|
|
|
543
506
|
# Collect tasks with descriptions, sorted by name
|
|
544
507
|
tasks = Rake.application.tasks
|
|
545
|
-
|
|
546
|
-
|
|
508
|
+
.select { |t| t.comment && t.name.start_with?('htm:') }
|
|
509
|
+
.sort_by(&:name)
|
|
547
510
|
|
|
548
511
|
# Filter by pattern if provided (matches task name)
|
|
549
512
|
if pattern
|
|
@@ -577,6 +540,44 @@ class HTM
|
|
|
577
540
|
puts "Run with: htm_mcp rake <task_name>"
|
|
578
541
|
puts "Example: htm_mcp rake htm:db:stats"
|
|
579
542
|
end
|
|
543
|
+
|
|
544
|
+
private
|
|
545
|
+
|
|
546
|
+
def suggestion_lines_for(msg)
|
|
547
|
+
if msg.include?("does not exist") && !msg.include?("role")
|
|
548
|
+
dbname = extract_dbname(ENV['HTM_DATABASE__URL'] || ENV.fetch('HTM_DATABASE__NAME', nil))
|
|
549
|
+
["Suggestion: The database does not exist. Create it with:",
|
|
550
|
+
" createdb #{dbname}",
|
|
551
|
+
"Then initialize the schema with:", " htm_mcp setup"]
|
|
552
|
+
elsif msg.include?("password authentication failed") || msg.include?("no password supplied")
|
|
553
|
+
["Suggestion: Check your database credentials.",
|
|
554
|
+
"Verify HTM_DATABASE__URL has correct username and password:",
|
|
555
|
+
" postgresql://USER:PASSWORD@localhost:5432/DATABASE"]
|
|
556
|
+
elsif msg.include?("connection refused") || msg.include?("could not connect")
|
|
557
|
+
["Suggestion: PostgreSQL server is not running or not accepting connections.",
|
|
558
|
+
"Start PostgreSQL with:",
|
|
559
|
+
" brew services start postgresql@17 # macOS with Homebrew",
|
|
560
|
+
" sudo systemctl start postgresql # Linux"]
|
|
561
|
+
elsif msg.include?("role") && msg.include?("does not exist")
|
|
562
|
+
["Suggestion: The database user does not exist. Create it with:",
|
|
563
|
+
" createuser -s YOUR_USERNAME"]
|
|
564
|
+
elsif msg.include?("permission denied")
|
|
565
|
+
["Suggestion: The user lacks permission to access this database.",
|
|
566
|
+
"Grant access or use a different user with appropriate privileges."]
|
|
567
|
+
elsif msg.include?("timeout") || msg.include?("timed out")
|
|
568
|
+
["Suggestion: Connection timed out. Check:",
|
|
569
|
+
" - PostgreSQL is running",
|
|
570
|
+
" - Firewall allows connections on port 5432",
|
|
571
|
+
" - Host address is correct"]
|
|
572
|
+
elsif msg.include?("extension") && msg.include?("vector")
|
|
573
|
+
["Suggestion: pgvector extension is not installed. Install it with:",
|
|
574
|
+
" brew install pgvector # macOS",
|
|
575
|
+
"Then enable it in your database:",
|
|
576
|
+
" psql -d DATABASE -c 'CREATE EXTENSION vector;'"]
|
|
577
|
+
else
|
|
578
|
+
["Suggestion: Run 'htm_mcp help' for configuration details."]
|
|
579
|
+
end
|
|
580
|
+
end
|
|
580
581
|
end
|
|
581
582
|
end
|
|
582
583
|
end
|
data/lib/htm/mcp/server.rb
CHANGED
|
@@ -28,12 +28,11 @@ class HTM
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def check_database_config!
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
31
|
+
return if ENV['HTM_DATABASE__URL'] || ENV['HTM_DATABASE__NAME']
|
|
32
|
+
warn "Error: Database not configured."
|
|
33
|
+
warn "Set HTM_DATABASE__URL or HTM_DATABASE__NAME environment variable."
|
|
34
|
+
warn "Run 'htm_mcp help' for details."
|
|
35
|
+
exit 1
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
def verify_database_connection!
|
data/lib/htm/mcp/tools.rb
CHANGED
|
@@ -88,44 +88,38 @@ class HTM
|
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
def call
|
|
91
|
-
htm
|
|
91
|
+
htm = Session.htm_instance
|
|
92
92
|
robot = HTM::Models::Robot[htm.robot_id]
|
|
93
93
|
Session.logger&.info "GetWorkingMemoryTool called for robot=#{htm.robot_name}"
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.all
|
|
102
|
-
.filter_map do |rn|
|
|
103
|
-
node = rn.node
|
|
104
|
-
next unless node # Exclude if node is nil (was deleted)
|
|
95
|
+
nodes = robot.robot_nodes_dataset
|
|
96
|
+
.in_working_memory
|
|
97
|
+
.eager(node: :tags)
|
|
98
|
+
.order(Sequel.desc(:last_remembered_at))
|
|
99
|
+
.all
|
|
100
|
+
.filter_map { |rn| format_working_memory_entry(rn) }
|
|
105
101
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
content: node.content,
|
|
109
|
-
tags: node.tags.map(&:name),
|
|
110
|
-
remember_count: rn.remember_count,
|
|
111
|
-
last_remembered_at: rn.last_remembered_at&.iso8601,
|
|
112
|
-
created_at: node.created_at.iso8601
|
|
113
|
-
}
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
Session.logger&.info "GetWorkingMemoryTool complete: #{working_memory_nodes.length} nodes in working memory"
|
|
117
|
-
|
|
118
|
-
{
|
|
119
|
-
success: true,
|
|
120
|
-
robot_id: htm.robot_id,
|
|
121
|
-
robot_name: htm.robot_name,
|
|
122
|
-
count: working_memory_nodes.length,
|
|
123
|
-
working_memory: working_memory_nodes
|
|
124
|
-
}.to_json
|
|
102
|
+
Session.logger&.info "GetWorkingMemoryTool complete: #{nodes.length} nodes in working memory"
|
|
103
|
+
{ success: true, robot_id: htm.robot_id, robot_name: htm.robot_name, count: nodes.length, working_memory: nodes }.to_json
|
|
125
104
|
rescue StandardError => e
|
|
126
105
|
Session.logger&.error "GetWorkingMemoryTool error: #{e.message}"
|
|
127
106
|
{ success: false, error: e.message, count: 0, working_memory: [] }.to_json
|
|
128
107
|
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def format_working_memory_entry(rn)
|
|
112
|
+
node = rn.node
|
|
113
|
+
return nil unless node
|
|
114
|
+
{
|
|
115
|
+
id: node.id,
|
|
116
|
+
content: node.content,
|
|
117
|
+
tags: node.tags.map(&:name),
|
|
118
|
+
remember_count: rn.remember_count,
|
|
119
|
+
last_remembered_at: rn.last_remembered_at&.iso8601,
|
|
120
|
+
created_at: node.created_at.iso8601
|
|
121
|
+
}
|
|
122
|
+
end
|
|
129
123
|
end
|
|
130
124
|
|
|
131
125
|
# Tool: Remember information
|
|
@@ -221,10 +215,10 @@ class HTM
|
|
|
221
215
|
Time.new(now.year, now.month, now.day)..now
|
|
222
216
|
when 'this week'
|
|
223
217
|
# 7 days ago
|
|
224
|
-
(now - 7 * 24 * 60 * 60)..now
|
|
218
|
+
(now - (7 * 24 * 60 * 60))..now
|
|
225
219
|
when 'this month'
|
|
226
220
|
# 30 days ago
|
|
227
|
-
(now - 30 * 24 * 60 * 60)..now
|
|
221
|
+
(now - (30 * 24 * 60 * 60))..now
|
|
228
222
|
else
|
|
229
223
|
# Try to parse as ISO8601 range (start..end)
|
|
230
224
|
if timeframe.include?('..')
|
|
@@ -438,14 +432,14 @@ class HTM
|
|
|
438
432
|
robot = HTM::Models::Robot[htm.robot_id]
|
|
439
433
|
Session.logger&.info "StatsTool called for robot=#{htm.robot_name}"
|
|
440
434
|
|
|
441
|
-
#
|
|
435
|
+
# NOTE: Node uses set_dataset to exclude deleted, so .count returns active nodes
|
|
442
436
|
total_nodes = HTM::Models::Node.count
|
|
443
437
|
deleted_nodes = HTM::Models::Node.deleted.count
|
|
444
438
|
nodes_with_embeddings = HTM::Models::Node.with_embeddings.count
|
|
445
439
|
nodes_with_tags = HTM::Models::Node
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
440
|
+
.join(:node_tags, node_id: :id)
|
|
441
|
+
.distinct
|
|
442
|
+
.count
|
|
449
443
|
total_tags = HTM::Models::Tag.count
|
|
450
444
|
total_robots = HTM::Models::Robot.count
|
|
451
445
|
|
data/lib/htm/migration.rb
CHANGED
|
@@ -41,24 +41,24 @@ class HTM
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
43
|
# Delegate common methods to db
|
|
44
|
-
def create_table(name,
|
|
45
|
-
db.create_table(name,
|
|
44
|
+
def create_table(name, **, &)
|
|
45
|
+
db.create_table(name, **, &)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
def drop_table(name, **
|
|
49
|
-
db.drop_table(name, **
|
|
48
|
+
def drop_table(name, **)
|
|
49
|
+
db.drop_table(name, **)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
def alter_table(name, &
|
|
53
|
-
db.alter_table(name, &
|
|
52
|
+
def alter_table(name, &)
|
|
53
|
+
db.alter_table(name, &)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
def add_index(table, columns, **
|
|
57
|
-
db.add_index(table, columns, **
|
|
56
|
+
def add_index(table, columns, **)
|
|
57
|
+
db.add_index(table, columns, **)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
def drop_index(table, columns, **
|
|
61
|
-
db.drop_index(table, columns, **
|
|
60
|
+
def drop_index(table, columns, **)
|
|
61
|
+
db.drop_index(table, columns, **)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def run(sql)
|
data/lib/htm/models/node.rb
CHANGED
|
@@ -43,7 +43,7 @@ class HTM
|
|
|
43
43
|
# Validations
|
|
44
44
|
def validate
|
|
45
45
|
super
|
|
46
|
-
validates_presence [
|
|
46
|
+
validates_presence %i[content content_hash]
|
|
47
47
|
validates_unique :content_hash
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -110,7 +110,6 @@ class HTM
|
|
|
110
110
|
|
|
111
111
|
# Select distance operator based on metric
|
|
112
112
|
operator = case distance.to_s
|
|
113
|
-
when "cosine" then "<=>"
|
|
114
113
|
when "euclidean", "l2" then "<->"
|
|
115
114
|
when "inner_product" then "<#>"
|
|
116
115
|
else "<=>"
|
|
@@ -356,7 +355,7 @@ class HTM
|
|
|
356
355
|
def to_hash
|
|
357
356
|
values.transform_keys(&:to_s)
|
|
358
357
|
end
|
|
359
|
-
|
|
358
|
+
alias attributes to_hash
|
|
360
359
|
end
|
|
361
360
|
end
|
|
362
361
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class HTM
|
|
4
|
+
module Models
|
|
5
|
+
# NodeRelationship model - weighted directed edge between two nodes
|
|
6
|
+
#
|
|
7
|
+
# Edges are stored in both directions so the CTE traversal only needs
|
|
8
|
+
# WHERE source_id IN (seeds) rather than an OR across both columns.
|
|
9
|
+
#
|
|
10
|
+
# Weights are Jaccard similarity scores for tag_cooccurrence edges:
|
|
11
|
+
# weight = |tags(A) ∩ tags(B)| / |tags(A) ∪ tags(B)|
|
|
12
|
+
#
|
|
13
|
+
class NodeRelationship < Sequel::Model(:node_relationships)
|
|
14
|
+
REL_TYPES = %w[related_to supports contradicts derived_from].freeze
|
|
15
|
+
ORIGINS = %w[tag_cooccurrence tag_hierarchy explicit].freeze
|
|
16
|
+
|
|
17
|
+
# Associations
|
|
18
|
+
many_to_one :source_node, class: 'HTM::Models::Node', key: :source_id
|
|
19
|
+
many_to_one :target_node, class: 'HTM::Models::Node', key: :target_id
|
|
20
|
+
|
|
21
|
+
# Plugins
|
|
22
|
+
plugin :validation_helpers
|
|
23
|
+
plugin :timestamps, update_on_create: true
|
|
24
|
+
|
|
25
|
+
# Validations
|
|
26
|
+
def validate
|
|
27
|
+
super
|
|
28
|
+
validates_presence %i[source_id target_id rel_type origin weight]
|
|
29
|
+
validates_includes REL_TYPES, :rel_type, allow_missing: true
|
|
30
|
+
validates_includes ORIGINS, :origin, allow_missing: true
|
|
31
|
+
validates_unique %i[source_id target_id rel_type], message: 'relationship already exists'
|
|
32
|
+
errors.add(:source_id, 'cannot relate a node to itself') if source_id && source_id == target_id
|
|
33
|
+
errors.add(:weight, 'must be between 0.0 and 1.0') if weight && !weight.between?(0.0, 1.0)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Dataset methods (scopes)
|
|
37
|
+
dataset_module do
|
|
38
|
+
def by_origin(origin)
|
|
39
|
+
where(origin: origin.to_s)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def by_rel_type(rel_type)
|
|
43
|
+
where(rel_type: rel_type.to_s)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def above_weight(min_weight)
|
|
47
|
+
where { weight >= min_weight }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def neighbors_of(node_id)
|
|
51
|
+
where(source_id: node_id).order(Sequel.desc(:weight))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def between_nodes(source_id, target_id)
|
|
55
|
+
where(source_id: source_id, target_id: target_id)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Hooks
|
|
60
|
+
def before_create
|
|
61
|
+
self.created_at ||= Time.now
|
|
62
|
+
self.updated_at ||= Time.now
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def before_save
|
|
67
|
+
self.updated_at = Time.now if changed_columns.any?
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/htm/models/node_tag.rb
CHANGED
|
@@ -15,8 +15,8 @@ class HTM
|
|
|
15
15
|
# Validations
|
|
16
16
|
def validate
|
|
17
17
|
super
|
|
18
|
-
validates_presence [
|
|
19
|
-
validates_unique [
|
|
18
|
+
validates_presence %i[node_id tag_id]
|
|
19
|
+
validates_unique %i[node_id tag_id], message: "already associated with this node"
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# Dataset methods (scopes)
|
|
@@ -19,8 +19,8 @@ class HTM
|
|
|
19
19
|
# Validations
|
|
20
20
|
def validate
|
|
21
21
|
super
|
|
22
|
-
validates_presence [
|
|
23
|
-
validates_unique [
|
|
22
|
+
validates_presence %i[robot_id node_id]
|
|
23
|
+
validates_unique %i[robot_id node_id], message: 'already linked to this node'
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# Dataset methods (scopes)
|
data/lib/htm/models/tag.rb
CHANGED
|
@@ -15,14 +15,14 @@ class HTM
|
|
|
15
15
|
plugin :timestamps, update_on_create: true
|
|
16
16
|
|
|
17
17
|
# Tag name format regex
|
|
18
|
-
TAG_FORMAT = /\A[a-z0-9
|
|
18
|
+
TAG_FORMAT = /\A[a-z0-9-]+(:[a-z0-9-]+)*\z/
|
|
19
19
|
|
|
20
20
|
# Validations
|
|
21
21
|
def validate
|
|
22
22
|
super
|
|
23
23
|
validates_presence :name
|
|
24
24
|
validates_format TAG_FORMAT, :name,
|
|
25
|
-
|
|
25
|
+
message: "must be lowercase with hyphens, using colons for hierarchy (e.g., 'database:postgresql:performance')"
|
|
26
26
|
validates_unique :name, message: "already exists"
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -233,8 +233,7 @@ class HTM
|
|
|
233
233
|
|
|
234
234
|
line_prefix = is_last_array.map { |was_last| was_last ? ' ' : '| ' }.join
|
|
235
235
|
|
|
236
|
-
|
|
237
|
-
result += "#{line_prefix}#{branch}#{key}\n"
|
|
236
|
+
result += "#{line_prefix}+-- #{key}\n"
|
|
238
237
|
|
|
239
238
|
children = node[key]
|
|
240
239
|
unless children.empty?
|
|
@@ -313,44 +312,58 @@ class HTM
|
|
|
313
312
|
|
|
314
313
|
# Generate SVG tree visualization (internal helper)
|
|
315
314
|
def self.generate_tree_svg(tree_data, positions, width, height, padding, node_width, node_height, title)
|
|
316
|
-
colors
|
|
317
|
-
|
|
315
|
+
colors = ['#3B82F6', '#8B5CF6', '#EC4899', '#F59E0B', '#10B981', '#6366F1']
|
|
318
316
|
svg_lines = []
|
|
319
317
|
svg_lines << %(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 #{width} #{height + 40}">)
|
|
320
318
|
svg_lines << ' <rect width="100%" height="100%" fill="transparent"/>'
|
|
321
|
-
|
|
322
|
-
svg_lines << %Q( <text x="#{width / 2}" y="25" text-anchor="middle" fill="#F3F4F6" font-family="system-ui, sans-serif" font-size="16" font-weight="bold">#{title}</text>)
|
|
319
|
+
svg_lines << svg_title_element(width, title)
|
|
323
320
|
|
|
324
321
|
positions.each do |path, pos|
|
|
325
322
|
parent_path = path.include?(':') ? path.split(':')[0..-2].join(':') : nil
|
|
326
323
|
next unless parent_path && positions[parent_path]
|
|
324
|
+
svg_lines.concat svg_node_elements(path, pos, positions[parent_path], colors, padding, node_width, node_height)
|
|
325
|
+
end
|
|
327
326
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
x2 = padding + (pos[:x] * (node_width + 40))
|
|
332
|
-
y2 = 40 + padding + (pos[:y] * (node_height + 20)) + (node_height / 2)
|
|
327
|
+
svg_lines << '</svg>'
|
|
328
|
+
svg_lines.join("\n")
|
|
329
|
+
end
|
|
333
330
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
331
|
+
def self.svg_title_element(width, title)
|
|
332
|
+
%( <text x="#{width / 2}" y="25" text-anchor="middle" fill="#F3F4F6" font-family="system-ui, sans-serif" font-size="16" font-weight="bold">#{title}</text>) # rubocop:disable Layout/LineLength
|
|
333
|
+
end
|
|
337
334
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
335
|
+
def self.svg_node_elements(path, pos, parent_pos, colors, padding, node_width, node_height)
|
|
336
|
+
c = svg_node_coords(pos, parent_pos, padding, node_width, node_height)
|
|
337
|
+
color = colors[path.count(':') % colors.size]
|
|
338
|
+
[
|
|
339
|
+
svg_edge_path(c[:x1], c[:y1], c[:x2], c[:y2]),
|
|
340
|
+
svg_node_rect(c[:x], c[:y], node_width, node_height, color),
|
|
341
|
+
svg_node_label(c[:x], c[:y], node_width, node_height, pos[:label])
|
|
342
|
+
]
|
|
343
|
+
end
|
|
341
344
|
|
|
342
|
-
|
|
343
|
-
|
|
345
|
+
def self.svg_node_coords(pos, parent_pos, padding, node_width, node_height)
|
|
346
|
+
x2 = padding + (pos[:x] * (node_width + 40))
|
|
347
|
+
y2 = 40 + padding + (pos[:y] * (node_height + 20)) + (node_height / 2)
|
|
348
|
+
{
|
|
349
|
+
x1: padding + (parent_pos[:x] * (node_width + 40)) + node_width,
|
|
350
|
+
y1: 40 + padding + (parent_pos[:y] * (node_height + 20)) + (node_height / 2),
|
|
351
|
+
x2: x2, y2: y2,
|
|
352
|
+
x: x2, y: 40 + padding + (pos[:y] * (node_height + 20))
|
|
353
|
+
}
|
|
354
|
+
end
|
|
344
355
|
|
|
345
|
-
|
|
356
|
+
def self.svg_edge_path(x1, y1, x2, y2)
|
|
357
|
+
mid_x = (x1 + x2) / 2
|
|
358
|
+
%( <path d="M#{x1},#{y1} C#{mid_x},#{y1} #{mid_x},#{y2} #{x2},#{y2}" stroke="#4B5563" stroke-width="2" fill="none"/>)
|
|
359
|
+
end
|
|
346
360
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
end
|
|
361
|
+
def self.svg_node_rect(x, y, node_width, node_height, color)
|
|
362
|
+
%( <rect x="#{x}" y="#{y}" width="#{node_width}" height="#{node_height}" rx="6" fill="#{color}" opacity="0.9"/>)
|
|
363
|
+
end
|
|
351
364
|
|
|
352
|
-
|
|
353
|
-
|
|
365
|
+
def self.svg_node_label(x, y, node_width, node_height, label)
|
|
366
|
+
%( <text x="#{x + (node_width / 2)}" y="#{y + (node_height / 2) + 4}" text-anchor="middle" fill="#FFFFFF" font-family="system-ui, sans-serif" font-size="11" font-weight="500">#{label}</text>) # rubocop:disable Layout/LineLength
|
|
354
367
|
end
|
|
355
368
|
|
|
356
369
|
# Instance methods
|