htm 0.0.1
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 +7 -0
- data/.architecture/decisions/adrs/001-use-postgresql-timescaledb-storage.md +227 -0
- data/.architecture/decisions/adrs/002-two-tier-memory-architecture.md +322 -0
- data/.architecture/decisions/adrs/003-ollama-default-embedding-provider.md +339 -0
- data/.architecture/decisions/adrs/004-multi-robot-shared-memory-hive-mind.md +374 -0
- data/.architecture/decisions/adrs/005-rag-based-retrieval-with-hybrid-search.md +443 -0
- data/.architecture/decisions/adrs/006-context-assembly-strategies.md +444 -0
- data/.architecture/decisions/adrs/007-working-memory-eviction-strategy.md +461 -0
- data/.architecture/decisions/adrs/008-robot-identification-system.md +550 -0
- data/.architecture/decisions/adrs/009-never-forget-explicit-deletion-only.md +570 -0
- data/.architecture/decisions/adrs/010-redis-working-memory-rejected.md +323 -0
- data/.architecture/decisions/adrs/011-database-side-embedding-generation-with-pgai.md +585 -0
- data/.architecture/decisions/adrs/012-llm-driven-ontology-topic-extraction.md +583 -0
- data/.architecture/decisions/adrs/013-activerecord-orm-and-many-to-many-tagging.md +299 -0
- data/.architecture/decisions/adrs/014-client-side-embedding-generation-workflow.md +569 -0
- data/.architecture/decisions/adrs/015-hierarchical-tag-ontology-and-llm-extraction.md +701 -0
- data/.architecture/decisions/adrs/016-async-embedding-and-tag-generation.md +694 -0
- data/.architecture/members.yml +144 -0
- data/.architecture/reviews/2025-10-29-llm-configuration-and-async-processing-review.md +1137 -0
- data/.architecture/reviews/initial-system-analysis.md +330 -0
- data/.envrc +32 -0
- data/.irbrc +145 -0
- data/CHANGELOG.md +150 -0
- data/COMMITS.md +196 -0
- data/LICENSE +21 -0
- data/README.md +1347 -0
- data/Rakefile +51 -0
- data/SETUP.md +268 -0
- data/config/database.yml +67 -0
- data/db/migrate/20250101000001_enable_extensions.rb +14 -0
- data/db/migrate/20250101000002_create_robots.rb +14 -0
- data/db/migrate/20250101000003_create_nodes.rb +42 -0
- data/db/migrate/20250101000005_create_tags.rb +38 -0
- data/db/migrate/20250101000007_add_node_vector_indexes.rb +30 -0
- data/db/schema.sql +473 -0
- data/db/seed_data/README.md +100 -0
- data/db/seed_data/presidents.md +136 -0
- data/db/seed_data/states.md +151 -0
- data/db/seeds.rb +208 -0
- data/dbdoc/README.md +173 -0
- data/dbdoc/public.node_stats.md +48 -0
- data/dbdoc/public.node_stats.svg +41 -0
- data/dbdoc/public.node_tags.md +40 -0
- data/dbdoc/public.node_tags.svg +112 -0
- data/dbdoc/public.nodes.md +54 -0
- data/dbdoc/public.nodes.svg +118 -0
- data/dbdoc/public.nodes_tags.md +39 -0
- data/dbdoc/public.nodes_tags.svg +112 -0
- data/dbdoc/public.ontology_structure.md +48 -0
- data/dbdoc/public.ontology_structure.svg +38 -0
- data/dbdoc/public.operations_log.md +42 -0
- data/dbdoc/public.operations_log.svg +130 -0
- data/dbdoc/public.relationships.md +39 -0
- data/dbdoc/public.relationships.svg +41 -0
- data/dbdoc/public.robot_activity.md +46 -0
- data/dbdoc/public.robot_activity.svg +35 -0
- data/dbdoc/public.robots.md +35 -0
- data/dbdoc/public.robots.svg +90 -0
- data/dbdoc/public.schema_migrations.md +29 -0
- data/dbdoc/public.schema_migrations.svg +26 -0
- data/dbdoc/public.tags.md +35 -0
- data/dbdoc/public.tags.svg +60 -0
- data/dbdoc/public.topic_relationships.md +45 -0
- data/dbdoc/public.topic_relationships.svg +32 -0
- data/dbdoc/schema.json +1437 -0
- data/dbdoc/schema.svg +154 -0
- data/docs/api/database.md +806 -0
- data/docs/api/embedding-service.md +532 -0
- data/docs/api/htm.md +797 -0
- data/docs/api/index.md +259 -0
- data/docs/api/long-term-memory.md +1096 -0
- data/docs/api/working-memory.md +665 -0
- data/docs/architecture/adrs/001-postgresql-timescaledb.md +314 -0
- data/docs/architecture/adrs/002-two-tier-memory.md +411 -0
- data/docs/architecture/adrs/003-ollama-embeddings.md +421 -0
- data/docs/architecture/adrs/004-hive-mind.md +437 -0
- data/docs/architecture/adrs/005-rag-retrieval.md +531 -0
- data/docs/architecture/adrs/006-context-assembly.md +496 -0
- data/docs/architecture/adrs/007-eviction-strategy.md +645 -0
- data/docs/architecture/adrs/008-robot-identification.md +625 -0
- data/docs/architecture/adrs/009-never-forget.md +648 -0
- data/docs/architecture/adrs/010-redis-working-memory-rejected.md +323 -0
- data/docs/architecture/adrs/011-pgai-integration.md +494 -0
- data/docs/architecture/adrs/index.md +215 -0
- data/docs/architecture/hive-mind.md +736 -0
- data/docs/architecture/index.md +351 -0
- data/docs/architecture/overview.md +538 -0
- data/docs/architecture/two-tier-memory.md +873 -0
- data/docs/assets/css/custom.css +83 -0
- data/docs/assets/images/htm-core-components.svg +63 -0
- data/docs/assets/images/htm-database-schema.svg +93 -0
- data/docs/assets/images/htm-hive-mind-architecture.svg +125 -0
- data/docs/assets/images/htm-importance-scoring-framework.svg +83 -0
- data/docs/assets/images/htm-layered-architecture.svg +71 -0
- data/docs/assets/images/htm-long-term-memory-architecture.svg +115 -0
- data/docs/assets/images/htm-working-memory-architecture.svg +120 -0
- data/docs/assets/images/htm.jpg +0 -0
- data/docs/assets/images/htm_demo.gif +0 -0
- data/docs/assets/js/mathjax.js +18 -0
- data/docs/assets/videos/htm_video.mp4 +0 -0
- data/docs/database_rake_tasks.md +322 -0
- data/docs/development/contributing.md +787 -0
- data/docs/development/index.md +336 -0
- data/docs/development/schema.md +596 -0
- data/docs/development/setup.md +719 -0
- data/docs/development/testing.md +819 -0
- data/docs/guides/adding-memories.md +824 -0
- data/docs/guides/context-assembly.md +1009 -0
- data/docs/guides/getting-started.md +577 -0
- data/docs/guides/index.md +118 -0
- data/docs/guides/long-term-memory.md +941 -0
- data/docs/guides/multi-robot.md +866 -0
- data/docs/guides/recalling-memories.md +927 -0
- data/docs/guides/search-strategies.md +953 -0
- data/docs/guides/working-memory.md +717 -0
- data/docs/index.md +214 -0
- data/docs/installation.md +477 -0
- data/docs/multi_framework_support.md +519 -0
- data/docs/quick-start.md +655 -0
- data/docs/setup_local_database.md +302 -0
- data/docs/using_rake_tasks_in_your_app.md +383 -0
- data/examples/basic_usage.rb +93 -0
- data/examples/cli_app/README.md +317 -0
- data/examples/cli_app/htm_cli.rb +270 -0
- data/examples/custom_llm_configuration.rb +183 -0
- data/examples/example_app/Rakefile +71 -0
- data/examples/example_app/app.rb +206 -0
- data/examples/sinatra_app/Gemfile +21 -0
- data/examples/sinatra_app/app.rb +335 -0
- data/lib/htm/active_record_config.rb +113 -0
- data/lib/htm/configuration.rb +342 -0
- data/lib/htm/database.rb +594 -0
- data/lib/htm/embedding_service.rb +115 -0
- data/lib/htm/errors.rb +34 -0
- data/lib/htm/job_adapter.rb +154 -0
- data/lib/htm/jobs/generate_embedding_job.rb +65 -0
- data/lib/htm/jobs/generate_tags_job.rb +82 -0
- data/lib/htm/long_term_memory.rb +965 -0
- data/lib/htm/models/node.rb +109 -0
- data/lib/htm/models/node_tag.rb +33 -0
- data/lib/htm/models/robot.rb +52 -0
- data/lib/htm/models/tag.rb +76 -0
- data/lib/htm/railtie.rb +76 -0
- data/lib/htm/sinatra.rb +157 -0
- data/lib/htm/tag_service.rb +135 -0
- data/lib/htm/tasks.rb +38 -0
- data/lib/htm/version.rb +5 -0
- data/lib/htm/working_memory.rb +182 -0
- data/lib/htm.rb +400 -0
- data/lib/tasks/db.rake +19 -0
- data/lib/tasks/htm.rake +147 -0
- data/lib/tasks/jobs.rake +312 -0
- data/mkdocs.yml +190 -0
- data/scripts/install_local_database.sh +309 -0
- metadata +341 -0
data/lib/htm/errors.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# HTM error classes
|
|
4
|
+
class HTM
|
|
5
|
+
# Base error class for all HTM errors
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Validation errors
|
|
9
|
+
class ValidationError < Error; end
|
|
10
|
+
|
|
11
|
+
# Resource exhausted errors (memory, tokens, etc.)
|
|
12
|
+
class ResourceExhaustedError < Error; end
|
|
13
|
+
|
|
14
|
+
# Resource not found errors
|
|
15
|
+
class NotFoundError < Error; end
|
|
16
|
+
|
|
17
|
+
# Embedding service errors
|
|
18
|
+
class EmbeddingError < Error; end
|
|
19
|
+
|
|
20
|
+
# Tag service errors
|
|
21
|
+
class TagError < Error; end
|
|
22
|
+
|
|
23
|
+
# Database operation errors
|
|
24
|
+
class DatabaseError < Error; end
|
|
25
|
+
|
|
26
|
+
# Query timeout errors
|
|
27
|
+
class QueryTimeoutError < DatabaseError; end
|
|
28
|
+
|
|
29
|
+
# Authorization errors
|
|
30
|
+
class AuthorizationError < Error; end
|
|
31
|
+
|
|
32
|
+
# Circuit breaker errors
|
|
33
|
+
class CircuitBreakerOpenError < EmbeddingError; end
|
|
34
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class HTM
|
|
4
|
+
# Job adapter for pluggable background job backends
|
|
5
|
+
#
|
|
6
|
+
# Supports multiple job backends to work seamlessly across different
|
|
7
|
+
# application types (CLI, Sinatra, Rails).
|
|
8
|
+
#
|
|
9
|
+
# Supported backends:
|
|
10
|
+
# - :active_job - Rails ActiveJob (recommended for Rails apps)
|
|
11
|
+
# - :sidekiq - Direct Sidekiq integration (recommended for Sinatra apps)
|
|
12
|
+
# - :inline - Synchronous execution (recommended for CLI and tests)
|
|
13
|
+
# - :thread - Background thread (legacy, for standalone apps)
|
|
14
|
+
#
|
|
15
|
+
# @example Configure job backend
|
|
16
|
+
# HTM.configure do |config|
|
|
17
|
+
# config.job_backend = :active_job
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Enqueue a job
|
|
21
|
+
# HTM::JobAdapter.enqueue(HTM::Jobs::GenerateEmbeddingJob, node_id: 123)
|
|
22
|
+
#
|
|
23
|
+
# @see ADR-016: Async Embedding and Tag Generation
|
|
24
|
+
#
|
|
25
|
+
module JobAdapter
|
|
26
|
+
class << self
|
|
27
|
+
# Enqueue a background job using the configured backend
|
|
28
|
+
#
|
|
29
|
+
# @param job_class [Class] Job class to enqueue (must respond to :perform)
|
|
30
|
+
# @param params [Hash] Parameters to pass to the job
|
|
31
|
+
# @return [void]
|
|
32
|
+
#
|
|
33
|
+
# @raise [HTM::Error] If job backend is unknown
|
|
34
|
+
#
|
|
35
|
+
def enqueue(job_class, **params)
|
|
36
|
+
backend = HTM.configuration.job_backend
|
|
37
|
+
|
|
38
|
+
case backend
|
|
39
|
+
when :active_job
|
|
40
|
+
enqueue_active_job(job_class, **params)
|
|
41
|
+
when :sidekiq
|
|
42
|
+
enqueue_sidekiq(job_class, **params)
|
|
43
|
+
when :inline
|
|
44
|
+
enqueue_inline(job_class, **params)
|
|
45
|
+
when :thread
|
|
46
|
+
enqueue_thread(job_class, **params)
|
|
47
|
+
else
|
|
48
|
+
raise HTM::Error, "Unknown job backend: #{backend}. Supported backends: :active_job, :sidekiq, :inline, :thread"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Enqueue job using ActiveJob (Rails)
|
|
55
|
+
def enqueue_active_job(job_class, **params)
|
|
56
|
+
unless defined?(ActiveJob)
|
|
57
|
+
raise HTM::Error, "ActiveJob is not available. Add 'activejob' gem or use a different backend."
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Convert job class to ActiveJob if needed
|
|
61
|
+
active_job_class = to_active_job_class(job_class)
|
|
62
|
+
active_job_class.perform_later(**params)
|
|
63
|
+
|
|
64
|
+
HTM.logger.debug "Enqueued #{job_class.name} via ActiveJob with params: #{params.inspect}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Enqueue job using Sidekiq directly
|
|
68
|
+
def enqueue_sidekiq(job_class, **params)
|
|
69
|
+
unless defined?(Sidekiq)
|
|
70
|
+
raise HTM::Error, "Sidekiq is not available. Add 'sidekiq' gem or use a different backend."
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Convert job class to Sidekiq worker if needed
|
|
74
|
+
sidekiq_class = to_sidekiq_worker(job_class)
|
|
75
|
+
sidekiq_class.perform_async(**params)
|
|
76
|
+
|
|
77
|
+
HTM.logger.debug "Enqueued #{job_class.name} via Sidekiq with params: #{params.inspect}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Execute job inline (synchronously)
|
|
81
|
+
def enqueue_inline(job_class, **params)
|
|
82
|
+
HTM.logger.debug "Executing #{job_class.name} inline with params: #{params.inspect}"
|
|
83
|
+
|
|
84
|
+
begin
|
|
85
|
+
job_class.perform(**params)
|
|
86
|
+
HTM.logger.debug "Completed #{job_class.name} inline execution"
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
HTM.logger.error "Inline job #{job_class.name} failed: #{e.class.name} - #{e.message}"
|
|
89
|
+
HTM.logger.debug e.backtrace.first(5).join("\n")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Execute job in background thread (legacy)
|
|
94
|
+
def enqueue_thread(job_class, **params)
|
|
95
|
+
Thread.new do
|
|
96
|
+
HTM.logger.debug "Executing #{job_class.name} in thread with params: #{params.inspect}"
|
|
97
|
+
|
|
98
|
+
begin
|
|
99
|
+
job_class.perform(**params)
|
|
100
|
+
HTM.logger.debug "Completed #{job_class.name} thread execution"
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
HTM.logger.error "Thread job #{job_class.name} failed: #{e.class.name} - #{e.message}"
|
|
103
|
+
HTM.logger.debug e.backtrace.first(5).join("\n")
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
HTM.logger.debug "Started thread for #{job_class.name}"
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
HTM.logger.error "Failed to start thread for #{job_class.name}: #{e.message}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Convert HTM job class to ActiveJob class
|
|
113
|
+
def to_active_job_class(job_class)
|
|
114
|
+
# If it's already an ActiveJob, return it
|
|
115
|
+
return job_class if job_class < ActiveJob::Base
|
|
116
|
+
|
|
117
|
+
# Create wrapper ActiveJob class
|
|
118
|
+
Class.new(ActiveJob::Base) do
|
|
119
|
+
queue_as :htm
|
|
120
|
+
|
|
121
|
+
define_method(:perform) do |**params|
|
|
122
|
+
job_class.perform(**params)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Set descriptive name
|
|
126
|
+
define_singleton_method(:name) do
|
|
127
|
+
"#{job_class.name}ActiveJobWrapper"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Convert HTM job class to Sidekiq worker
|
|
133
|
+
def to_sidekiq_worker(job_class)
|
|
134
|
+
# If it's already a Sidekiq worker, return it
|
|
135
|
+
return job_class if job_class.included_modules.include?(Sidekiq::Worker)
|
|
136
|
+
|
|
137
|
+
# Create wrapper Sidekiq worker
|
|
138
|
+
Class.new do
|
|
139
|
+
include Sidekiq::Worker
|
|
140
|
+
sidekiq_options queue: :htm, retry: 3
|
|
141
|
+
|
|
142
|
+
define_method(:perform) do |**params|
|
|
143
|
+
job_class.perform(**params)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Set descriptive name
|
|
147
|
+
define_singleton_method(:name) do
|
|
148
|
+
"#{job_class.name}SidekiqWrapper"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../errors'
|
|
4
|
+
require_relative '../models/node'
|
|
5
|
+
require_relative '../embedding_service'
|
|
6
|
+
|
|
7
|
+
class HTM
|
|
8
|
+
module Jobs
|
|
9
|
+
# Background job to generate and store vector embeddings for nodes
|
|
10
|
+
#
|
|
11
|
+
# This job is enqueued after a node is saved to avoid blocking the
|
|
12
|
+
# main request path. It generates embeddings asynchronously and updates
|
|
13
|
+
# the node record with the embedding vector.
|
|
14
|
+
#
|
|
15
|
+
# @see ADR-016: Async Embedding and Tag Generation
|
|
16
|
+
#
|
|
17
|
+
class GenerateEmbeddingJob
|
|
18
|
+
# Generate embedding for a node
|
|
19
|
+
#
|
|
20
|
+
# Uses the configured embedding generator (HTM.embed) which delegates
|
|
21
|
+
# to the application-provided or default RubyLLM implementation.
|
|
22
|
+
#
|
|
23
|
+
# @param node_id [Integer] ID of the node to process
|
|
24
|
+
#
|
|
25
|
+
def self.perform(node_id:)
|
|
26
|
+
node = HTM::Models::Node.find_by(id: node_id)
|
|
27
|
+
|
|
28
|
+
unless node
|
|
29
|
+
HTM.logger.warn "GenerateEmbeddingJob: Node #{node_id} not found"
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Skip if already has embedding
|
|
34
|
+
if node.embedding.present?
|
|
35
|
+
HTM.logger.debug "GenerateEmbeddingJob: Node #{node_id} already has embedding, skipping"
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
HTM.logger.debug "GenerateEmbeddingJob: Generating embedding for node #{node_id}"
|
|
41
|
+
|
|
42
|
+
# Generate and process embedding using EmbeddingService
|
|
43
|
+
result = HTM::EmbeddingService.generate(node.content)
|
|
44
|
+
|
|
45
|
+
# Update node with processed embedding
|
|
46
|
+
node.update!(
|
|
47
|
+
embedding: result[:storage_embedding],
|
|
48
|
+
embedding_dimension: result[:dimension]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
HTM.logger.info "GenerateEmbeddingJob: Successfully generated embedding for node #{node_id} (#{result[:dimension]} dimensions)"
|
|
52
|
+
|
|
53
|
+
rescue HTM::EmbeddingError => e
|
|
54
|
+
# Log embedding-specific errors
|
|
55
|
+
HTM.logger.error "GenerateEmbeddingJob: Embedding generation failed for node #{node_id}: #{e.message}"
|
|
56
|
+
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
# Log unexpected errors
|
|
59
|
+
HTM.logger.error "GenerateEmbeddingJob: Unexpected error for node #{node_id}: #{e.class.name} - #{e.message}"
|
|
60
|
+
HTM.logger.debug e.backtrace.first(5).join("\n")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../errors'
|
|
4
|
+
require_relative '../models/node'
|
|
5
|
+
require_relative '../models/tag'
|
|
6
|
+
require_relative '../models/node_tag'
|
|
7
|
+
require_relative '../tag_service'
|
|
8
|
+
|
|
9
|
+
class HTM
|
|
10
|
+
module Jobs
|
|
11
|
+
# Background job to generate and associate tags for nodes
|
|
12
|
+
#
|
|
13
|
+
# This job is enqueued after a node is saved to avoid blocking the
|
|
14
|
+
# main request path. It uses LLM to extract hierarchical tags from
|
|
15
|
+
# node content and creates the necessary database associations.
|
|
16
|
+
#
|
|
17
|
+
# @see ADR-016: Async Embedding and Tag Generation
|
|
18
|
+
# @see ADR-015: Hierarchical Tag Ontology and LLM Extraction
|
|
19
|
+
#
|
|
20
|
+
class GenerateTagsJob
|
|
21
|
+
# Generate tags for a node
|
|
22
|
+
#
|
|
23
|
+
# Uses the configured tag extractor (HTM.extract_tags) which delegates
|
|
24
|
+
# to the application-provided or default RubyLLM implementation.
|
|
25
|
+
#
|
|
26
|
+
# @param node_id [Integer] ID of the node to process
|
|
27
|
+
#
|
|
28
|
+
def self.perform(node_id:)
|
|
29
|
+
node = HTM::Models::Node.find_by(id: node_id)
|
|
30
|
+
|
|
31
|
+
unless node
|
|
32
|
+
HTM.logger.warn "GenerateTagsJob: Node #{node_id} not found"
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
HTM.logger.debug "GenerateTagsJob: Extracting tags for node #{node_id}"
|
|
38
|
+
|
|
39
|
+
# Get existing ontology for context (sample of recent tags)
|
|
40
|
+
existing_ontology = HTM::Models::Tag
|
|
41
|
+
.order(created_at: :desc)
|
|
42
|
+
.limit(100)
|
|
43
|
+
.pluck(:name)
|
|
44
|
+
|
|
45
|
+
# Extract and validate tags using TagService
|
|
46
|
+
tag_names = HTM::TagService.extract(node.content, existing_ontology: existing_ontology)
|
|
47
|
+
|
|
48
|
+
if tag_names.empty?
|
|
49
|
+
HTM.logger.debug "GenerateTagsJob: No tags extracted for node #{node_id}"
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Create or find tags and associate with node
|
|
54
|
+
tag_names.each do |tag_name|
|
|
55
|
+
tag = HTM::Models::Tag.find_or_create_by!(name: tag_name)
|
|
56
|
+
|
|
57
|
+
# Create association if it doesn't exist
|
|
58
|
+
HTM::Models::NodeTag.find_or_create_by!(
|
|
59
|
+
node_id: node.id,
|
|
60
|
+
tag_id: tag.id
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
HTM.logger.info "GenerateTagsJob: Successfully generated #{tag_names.length} tags for node #{node_id}: #{tag_names.join(', ')}"
|
|
65
|
+
|
|
66
|
+
rescue HTM::TagError => e
|
|
67
|
+
# Log tag-specific errors
|
|
68
|
+
HTM.logger.error "GenerateTagsJob: Tag generation failed for node #{node_id}: #{e.message}"
|
|
69
|
+
|
|
70
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
71
|
+
# Log validation errors
|
|
72
|
+
HTM.logger.error "GenerateTagsJob: Database validation failed for node #{node_id}: #{e.message}"
|
|
73
|
+
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
# Log unexpected errors
|
|
76
|
+
HTM.logger.error "GenerateTagsJob: Unexpected error for node #{node_id}: #{e.class.name} - #{e.message}"
|
|
77
|
+
HTM.logger.debug e.backtrace.first(5).join("\n")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|