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.
Files changed (161) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +2 -3
  3. data/.rubocop.yml +184 -0
  4. data/CHANGELOG.md +46 -0
  5. data/README.md +2 -0
  6. data/Rakefile +93 -12
  7. data/db/migrate/00008_create_node_relationships.rb +54 -0
  8. data/db/migrate/00009_fix_node_relationships_column_types.rb +17 -0
  9. data/db/schema.sql +124 -1
  10. data/docs/api/database.md +35 -57
  11. data/docs/api/embedding-service.md +1 -1
  12. data/docs/api/index.md +26 -15
  13. data/docs/api/working-memory.md +8 -8
  14. data/docs/architecture/index.md +5 -7
  15. data/docs/architecture/overview.md +5 -8
  16. data/docs/assets/images/htm-architecture-overview.svg +1 -1
  17. data/docs/assets/images/htm-context-assembly-flow.svg +2 -2
  18. data/docs/assets/images/htm-layered-architecture.svg +3 -3
  19. data/docs/assets/images/two-tier-memory-architecture.svg +1 -1
  20. data/docs/database/README.md +1 -0
  21. data/docs/database_rake_tasks.md +20 -28
  22. data/docs/development/contributing.md +5 -5
  23. data/docs/development/index.md +4 -7
  24. data/docs/development/schema.md +71 -1
  25. data/docs/development/setup.md +40 -82
  26. data/docs/development/testing.md +1 -1
  27. data/docs/examples/file-loading.md +4 -4
  28. data/docs/examples/mcp-client.md +1 -1
  29. data/docs/getting-started/quick-start.md +4 -4
  30. data/docs/guides/adding-memories.md +14 -1
  31. data/docs/guides/configuration.md +5 -5
  32. data/docs/guides/context-assembly.md +4 -4
  33. data/docs/guides/file-loading.md +12 -12
  34. data/docs/guides/getting-started.md +2 -2
  35. data/docs/guides/long-term-memory.md +7 -27
  36. data/docs/guides/propositions.md +20 -19
  37. data/docs/guides/recalling-memories.md +5 -5
  38. data/docs/guides/tags.md +18 -13
  39. data/docs/multi_framework_support.md +1 -1
  40. data/docs/robots/hive-mind.md +1 -1
  41. data/docs/robots/multi-robot.md +2 -2
  42. data/docs/robots/robot-groups.md +1 -1
  43. data/docs/robots/two-tier-memory.md +72 -94
  44. data/docs/setup_local_database.md +8 -54
  45. data/docs/using_rake_tasks_in_your_app.md +6 -6
  46. data/examples/01_basic_usage.rb +1 -0
  47. data/examples/03_custom_llm_configuration.rb +1 -0
  48. data/examples/04_file_loader_usage.rb +1 -0
  49. data/examples/05_timeframe_demo.rb +1 -0
  50. data/examples/06_example_app/app.rb +1 -0
  51. data/examples/07_cli_app/htm_cli.rb +1 -0
  52. data/examples/09_mcp_client.rb +1 -0
  53. data/examples/10_telemetry/demo.rb +1 -0
  54. data/examples/11_robot_groups/multi_process.rb +1 -0
  55. data/examples/11_robot_groups/same_process.rb +1 -0
  56. data/examples/12_rails_app/.envrc +12 -0
  57. data/examples/12_rails_app/Gemfile +8 -3
  58. data/examples/12_rails_app/Gemfile.lock +94 -89
  59. data/examples/12_rails_app/README.md +70 -19
  60. data/examples/12_rails_app/app/controllers/application_controller.rb +6 -0
  61. data/examples/12_rails_app/app/controllers/chats_controller.rb +305 -0
  62. data/examples/12_rails_app/app/controllers/dashboard_controller.rb +3 -0
  63. data/examples/12_rails_app/app/controllers/files_controller.rb +17 -2
  64. data/examples/12_rails_app/app/controllers/home_controller.rb +8 -0
  65. data/examples/12_rails_app/app/controllers/memories_controller.rb +9 -4
  66. data/examples/12_rails_app/app/controllers/messages_controller.rb +214 -0
  67. data/examples/12_rails_app/app/controllers/robots_controller.rb +11 -1
  68. data/examples/12_rails_app/app/controllers/tags_controller.rb +14 -1
  69. data/examples/12_rails_app/app/javascript/application.js +1 -1
  70. data/examples/12_rails_app/app/models/application_record.rb +5 -0
  71. data/examples/12_rails_app/app/models/chat.rb +36 -0
  72. data/examples/12_rails_app/app/models/message.rb +5 -0
  73. data/examples/12_rails_app/app/models/model.rb +5 -0
  74. data/examples/12_rails_app/app/models/tool_call.rb +5 -0
  75. data/examples/12_rails_app/app/views/chats/index.html.erb +61 -0
  76. data/examples/12_rails_app/app/views/chats/show.html.erb +213 -0
  77. data/examples/12_rails_app/app/views/dashboard/index.html.erb +3 -0
  78. data/examples/12_rails_app/app/views/files/index.html.erb +10 -5
  79. data/examples/12_rails_app/app/views/files/new.html.erb +4 -2
  80. data/examples/12_rails_app/app/views/files/show.html.erb +19 -3
  81. data/examples/12_rails_app/app/views/home/index.html.erb +45 -0
  82. data/examples/12_rails_app/app/views/layouts/application.html.erb +20 -18
  83. data/examples/12_rails_app/app/views/memories/_memory_card.html.erb +1 -1
  84. data/examples/12_rails_app/app/views/memories/deleted.html.erb +3 -1
  85. data/examples/12_rails_app/app/views/memories/edit.html.erb +2 -0
  86. data/examples/12_rails_app/app/views/memories/index.html.erb +2 -0
  87. data/examples/12_rails_app/app/views/memories/new.html.erb +2 -0
  88. data/examples/12_rails_app/app/views/memories/show.html.erb +4 -2
  89. data/examples/12_rails_app/app/views/messages/_message.html.erb +20 -0
  90. data/examples/12_rails_app/app/views/robots/index.html.erb +2 -0
  91. data/examples/12_rails_app/app/views/robots/new.html.erb +2 -0
  92. data/examples/12_rails_app/app/views/robots/show.html.erb +2 -0
  93. data/examples/12_rails_app/app/views/search/index.html.erb +59 -8
  94. data/examples/12_rails_app/app/views/shared/_navbar.html.erb +75 -29
  95. data/examples/12_rails_app/app/views/tags/index.html.erb +2 -0
  96. data/examples/12_rails_app/app/views/tags/show.html.erb +3 -1
  97. data/examples/12_rails_app/config/application.rb +1 -1
  98. data/examples/12_rails_app/config/database.yml +9 -5
  99. data/examples/12_rails_app/config/importmap.rb +1 -1
  100. data/examples/12_rails_app/config/initializers/htm.rb +9 -2
  101. data/examples/12_rails_app/config/initializers/ruby_llm.rb +33 -0
  102. data/examples/12_rails_app/config/routes.rb +39 -23
  103. data/examples/12_rails_app/db/migrate/20250124000001_create_ruby_llm_tables.rb +34 -0
  104. data/examples/12_rails_app/db/migrate/20250124000002_create_models_table.rb +28 -0
  105. data/examples/12_rails_app/db/schema.rb +67 -0
  106. data/examples/examples_helper.rb +25 -0
  107. data/lib/htm/circuit_breaker.rb +5 -6
  108. data/lib/htm/config/builder.rb +12 -12
  109. data/lib/htm/config/database.rb +21 -27
  110. data/lib/htm/config/defaults.yml +25 -13
  111. data/lib/htm/config/validator.rb +12 -18
  112. data/lib/htm/config.rb +93 -173
  113. data/lib/htm/database.rb +193 -199
  114. data/lib/htm/embedding_service.rb +4 -9
  115. data/lib/htm/integrations/sinatra.rb +7 -7
  116. data/lib/htm/job_adapter.rb +14 -21
  117. data/lib/htm/jobs/generate_embedding_job.rb +28 -44
  118. data/lib/htm/jobs/generate_propositions_job.rb +29 -55
  119. data/lib/htm/jobs/generate_relationships_job.rb +137 -0
  120. data/lib/htm/jobs/generate_tags_job.rb +45 -67
  121. data/lib/htm/loaders/markdown_loader.rb +65 -112
  122. data/lib/htm/long_term_memory/fulltext_search.rb +1 -1
  123. data/lib/htm/long_term_memory/hybrid_search.rb +300 -128
  124. data/lib/htm/long_term_memory/node_operations.rb +2 -2
  125. data/lib/htm/long_term_memory/relevance_scorer.rb +100 -68
  126. data/lib/htm/long_term_memory/tag_operations.rb +87 -120
  127. data/lib/htm/long_term_memory/vector_search.rb +1 -1
  128. data/lib/htm/long_term_memory.rb +2 -1
  129. data/lib/htm/mcp/cli.rb +59 -58
  130. data/lib/htm/mcp/server.rb +5 -6
  131. data/lib/htm/mcp/tools.rb +30 -36
  132. data/lib/htm/migration.rb +10 -10
  133. data/lib/htm/models/node.rb +2 -3
  134. data/lib/htm/models/node_relationship.rb +72 -0
  135. data/lib/htm/models/node_tag.rb +2 -2
  136. data/lib/htm/models/robot_node.rb +2 -2
  137. data/lib/htm/models/tag.rb +41 -28
  138. data/lib/htm/observability.rb +45 -51
  139. data/lib/htm/proposition_service.rb +3 -7
  140. data/lib/htm/query_cache.rb +13 -15
  141. data/lib/htm/railtie.rb +1 -2
  142. data/lib/htm/robot_group.rb +9 -9
  143. data/lib/htm/sequel_config.rb +1 -0
  144. data/lib/htm/sql_builder.rb +1 -1
  145. data/lib/htm/tag_service.rb +2 -6
  146. data/lib/htm/timeframe.rb +4 -5
  147. data/lib/htm/timeframe_extractor.rb +42 -83
  148. data/lib/htm/version.rb +1 -1
  149. data/lib/htm/workflows/remember_workflow.rb +112 -115
  150. data/lib/htm/working_memory.rb +21 -26
  151. data/lib/htm.rb +103 -116
  152. data/lib/tasks/db.rake +0 -2
  153. data/lib/tasks/doc.rake +14 -13
  154. data/lib/tasks/files.rake +5 -12
  155. data/lib/tasks/htm.rake +70 -71
  156. data/lib/tasks/jobs.rake +41 -47
  157. data/lib/tasks/tags.rake +3 -8
  158. metadata +28 -106
  159. data/lib/htm/config/section.rb +0 -74
  160. data/lib/htm/loaders/defaults_loader.rb +0 -166
  161. data/lib/htm/loaders/xdg_config_loader.rb +0 -116
data/lib/htm/config.rb CHANGED
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'anyway_config'
3
+ require 'myway_config'
4
4
  require 'logger'
5
- require 'yaml'
6
5
 
7
6
  # Define Config class first to establish superclass
8
7
  class HTM
9
- class Config < Anyway::Config
8
+ class Config < MywayConfig::Base
10
9
  end
11
10
  end
12
11
 
13
- require_relative 'config/section'
14
12
  require_relative 'config/validator'
15
13
  require_relative 'config/database'
16
14
  require_relative 'config/builder'
@@ -61,35 +59,11 @@ class HTM
61
59
 
62
60
  config_name :htm
63
61
  env_prefix :htm
62
+ defaults_path File.expand_path('config/defaults.yml', __dir__)
64
63
 
65
- # ==========================================================================
66
- # Schema Definition (loaded from defaults.yml - single source of truth)
67
- # ==========================================================================
68
-
69
- # Path to bundled defaults file (defines both schema and default values)
70
- DEFAULTS_PATH = File.expand_path('config/defaults.yml', __dir__).freeze
71
-
72
- # Load schema from defaults.yml at class definition time
73
- begin
74
- defaults_content = File.read(DEFAULTS_PATH)
75
- raw_yaml = YAML.safe_load(
76
- defaults_content,
77
- permitted_classes: [Symbol],
78
- symbolize_names: true,
79
- aliases: true
80
- ) || {}
81
- SCHEMA = raw_yaml[:defaults] || {}
82
- rescue StandardError => e
83
- warn "HTM: Could not load schema from #{DEFAULTS_PATH}: #{e.message}"
84
- SCHEMA = {}
85
- end
86
-
87
- # Nested section attributes (defined as hashes, converted to ConfigSection)
88
- attr_config :database, :service, :embedding, :tag, :proposition,
89
- :chunking, :circuit_breaker, :relevance, :job, :providers
90
-
91
- # Top-level scalar attributes
92
- attr_config :week_start, :connection_timeout, :telemetry_enabled, :log_level
64
+ # Auto-configure attributes and coercions from defaults.yml schema
65
+ # This replaces manual attr_config and coerce_types declarations
66
+ auto_configure!
93
67
 
94
68
  # Custom environment detection: HTM_ENV > RAILS_ENV > RACK_ENV > 'development'
95
69
  class << self
@@ -102,60 +76,6 @@ class HTM
102
76
  end
103
77
  end
104
78
 
105
- # ==========================================================================
106
- # Type Coercion
107
- # ==========================================================================
108
-
109
- TO_SYMBOL = ->(v) { v.nil? ? nil : v.to_s.to_sym }
110
-
111
- # Create a coercion that merges incoming value with SCHEMA defaults for a section.
112
- # This ensures env vars like HTM_DATABASE__URL don't lose other defaults.
113
- def self.config_section_with_defaults(section_key)
114
- defaults = SCHEMA[section_key] || {}
115
- ->(v) {
116
- return v if v.is_a?(ConfigSection)
117
- incoming = v || {}
118
- # Deep merge: defaults first, then overlay incoming values
119
- merged = deep_merge_hashes(defaults.dup, incoming)
120
- ConfigSection.new(merged)
121
- }
122
- end
123
-
124
- # Deep merge helper for coercion
125
- def self.deep_merge_hashes(base, overlay)
126
- base.merge(overlay) do |_key, old_val, new_val|
127
- if old_val.is_a?(Hash) && new_val.is_a?(Hash)
128
- deep_merge_hashes(old_val, new_val)
129
- else
130
- new_val.nil? ? old_val : new_val
131
- end
132
- end
133
- end
134
-
135
- coerce_types(
136
- # Nested sections -> ConfigSection objects (with SCHEMA defaults merged)
137
- database: config_section_with_defaults(:database),
138
- service: config_section_with_defaults(:service),
139
- embedding: config_section_with_defaults(:embedding),
140
- tag: config_section_with_defaults(:tag),
141
- proposition: config_section_with_defaults(:proposition),
142
- chunking: config_section_with_defaults(:chunking),
143
- circuit_breaker: config_section_with_defaults(:circuit_breaker),
144
- relevance: config_section_with_defaults(:relevance),
145
- job: config_section_with_defaults(:job),
146
- providers: config_section_with_defaults(:providers),
147
-
148
- # Top-level symbols
149
- week_start: TO_SYMBOL,
150
- log_level: TO_SYMBOL,
151
-
152
- # Top-level integers
153
- connection_timeout: :integer,
154
-
155
- # Top-level booleans
156
- telemetry_enabled: :boolean
157
- )
158
-
159
79
  # ==========================================================================
160
80
  # Validation
161
81
  # ==========================================================================
@@ -179,8 +99,7 @@ class HTM
179
99
  # Callable Accessors (not loaded from config sources)
180
100
  # ==========================================================================
181
101
 
182
- attr_accessor :embedding_generator, :tag_extractor, :proposition_extractor
183
- attr_accessor :token_counter, :logger
102
+ attr_accessor :embedding_generator, :tag_extractor, :proposition_extractor, :token_counter, :logger
184
103
 
185
104
  # ==========================================================================
186
105
  # Instance Methods
@@ -256,11 +175,11 @@ class HTM
256
175
 
257
176
  # Chunking convenience accessors
258
177
  def chunk_size
259
- chunking.size.to_i
178
+ chunking.chunk_size.to_i
260
179
  end
261
180
 
262
181
  def chunk_overlap
263
- chunking.overlap.to_i
182
+ chunking.chunk_overlap.to_i
264
183
  end
265
184
 
266
185
  # Circuit breaker convenience accessors
@@ -375,17 +294,8 @@ class HTM
375
294
  # Environment Helpers
376
295
  # ==========================================================================
377
296
 
378
- def test?
379
- self.class.env == 'test'
380
- end
381
-
382
- def development?
383
- self.class.env == 'development'
384
- end
385
-
386
- def production?
387
- self.class.env == 'production'
388
- end
297
+ # NOTE: test?, development?, production? are auto-generated by MywayConfig::Base
298
+ # based on environment keys in defaults.yml
389
299
 
390
300
  def environment
391
301
  self.class.env
@@ -396,17 +306,16 @@ class HTM
396
306
  # ==========================================================================
397
307
 
398
308
  # Returns list of valid environment names from bundled defaults
309
+ # Inherited from MywayConfig::Base - delegates to DefaultsLoader
399
310
  #
400
311
  # @return [Array<Symbol>] valid environment names (e.g., [:development, :production, :test])
401
- def self.valid_environments
402
- HTM::Loaders::DefaultsLoader.valid_environments
403
- end
312
+ # Note: valid_environments is inherited from MywayConfig::Base
404
313
 
405
314
  # Check if current environment is valid (defined in config)
406
315
  #
407
316
  # @return [Boolean] true if environment has a config section
408
317
  def self.valid_environment?
409
- HTM::Loaders::DefaultsLoader.valid_environment?(env)
318
+ MywayConfig::Loaders::DefaultsLoader.valid_environment?(config_name, env)
410
319
  end
411
320
 
412
321
  # Validate that the current environment is configured
@@ -415,13 +324,13 @@ class HTM
415
324
  # @return [true] if environment is valid
416
325
  def self.validate_environment!
417
326
  current = env
418
- return true if HTM::Loaders::DefaultsLoader.valid_environment?(current)
327
+ return true if valid_environment?
419
328
 
420
- valid = valid_environments.map(&:to_s).join(', ')
329
+ valid = valid_environments.join(', ')
421
330
  raise HTM::ConfigurationError,
422
- "Invalid environment '#{current}'. " \
423
- "Valid environments are: #{valid}. " \
424
- "Set HTM_ENV to a valid environment or add a '#{current}:' section to your config."
331
+ "Invalid environment '#{current}'. " \
332
+ "Valid environments are: #{valid}. " \
333
+ "Set HTM_ENV to a valid environment or add a '#{current}:' section to your config."
425
334
  end
426
335
 
427
336
  # Instance method delegates
@@ -438,21 +347,21 @@ class HTM
438
347
  # ==========================================================================
439
348
 
440
349
  def self.xdg_config_paths
441
- HTM::Loaders::XdgConfigLoader.config_paths
350
+ MywayConfig::Loaders::XdgConfigLoader.config_paths(config_name)
442
351
  end
443
352
 
444
353
  def self.xdg_config_file
445
- xdg_home = ENV['XDG_CONFIG_HOME']
354
+ xdg_home = ENV.fetch('XDG_CONFIG_HOME', nil)
446
355
  base = if xdg_home && !xdg_home.empty?
447
- xdg_home
448
- else
449
- File.expand_path('~/.config')
450
- end
356
+ xdg_home
357
+ else
358
+ File.expand_path('~/.config')
359
+ end
451
360
  File.join(base, 'htm', 'htm.yml')
452
361
  end
453
362
 
454
363
  def self.active_xdg_config_file
455
- HTM::Loaders::XdgConfigLoader.find_config_file('htm')
364
+ MywayConfig::Loaders::XdgConfigLoader.find_config_file(config_name)
456
365
  end
457
366
 
458
367
  # ==========================================================================
@@ -468,42 +377,8 @@ class HTM
468
377
 
469
378
  def configure_ruby_llm(provider = nil)
470
379
  require 'ruby_llm'
471
-
472
380
  provider ||= embedding_provider
473
-
474
- RubyLLM.configure do |config|
475
- case provider
476
- when :openai
477
- config.openai_api_key = openai_api_key if openai_api_key
478
- config.openai_organization = openai_organization if openai_organization && config.respond_to?(:openai_organization=)
479
- config.openai_project = openai_project if openai_project && config.respond_to?(:openai_project=)
480
- when :anthropic
481
- config.anthropic_api_key = anthropic_api_key if anthropic_api_key
482
- when :gemini
483
- config.gemini_api_key = gemini_api_key if gemini_api_key
484
- when :azure
485
- config.azure_api_key = azure_api_key if azure_api_key && config.respond_to?(:azure_api_key=)
486
- config.azure_endpoint = azure_endpoint if azure_endpoint && config.respond_to?(:azure_endpoint=)
487
- config.azure_api_version = azure_api_version if azure_api_version && config.respond_to?(:azure_api_version=)
488
- when :ollama
489
- ollama_api_base = if ollama_url.end_with?('/v1') || ollama_url.end_with?('/v1/')
490
- ollama_url.sub(%r{/+$}, '')
491
- else
492
- "#{ollama_url.sub(%r{/+$}, '')}/v1"
493
- end
494
- config.ollama_api_base = ollama_api_base
495
- when :huggingface
496
- config.huggingface_api_key = huggingface_api_key if huggingface_api_key && config.respond_to?(:huggingface_api_key=)
497
- when :openrouter
498
- config.openrouter_api_key = openrouter_api_key if openrouter_api_key && config.respond_to?(:openrouter_api_key=)
499
- when :bedrock
500
- config.bedrock_api_key = bedrock_access_key if bedrock_access_key && config.respond_to?(:bedrock_api_key=)
501
- config.bedrock_secret_key = bedrock_secret_key if bedrock_secret_key && config.respond_to?(:bedrock_secret_key=)
502
- config.bedrock_region = bedrock_region if bedrock_region && config.respond_to?(:bedrock_region=)
503
- when :deepseek
504
- config.deepseek_api_key = deepseek_api_key if deepseek_api_key && config.respond_to?(:deepseek_api_key=)
505
- end
506
- end
381
+ RubyLLM.configure { |config| apply_provider_config(config, provider) }
507
382
  end
508
383
 
509
384
  def refresh_ollama_models!
@@ -543,26 +418,73 @@ class HTM
543
418
  # ==========================================================================
544
419
 
545
420
  def coerce_nested_types
546
- # Ensure nested provider sections are ConfigSections
547
- if providers.is_a?(ConfigSection)
548
- %i[openai anthropic gemini azure ollama huggingface openrouter bedrock deepseek].each do |provider|
549
- value = providers[provider]
550
- providers[provider] = ConfigSection.new(value) if value.is_a?(Hash)
551
- end
552
- end
421
+ coerce_provider_sections
422
+ coerce_database_integers
423
+ end
553
424
 
554
- # Coerce database numeric fields to integers (env vars are always strings)
555
- if database&.port && !database.port.is_a?(Integer)
556
- database.port = database.port.to_i
557
- end
558
- if database&.pool_size && !database.pool_size.is_a?(Integer)
559
- database.pool_size = database.pool_size.to_i
560
- end
561
- if database&.timeout && !database.timeout.is_a?(Integer)
562
- database.timeout = database.timeout.to_i
425
+ def coerce_provider_sections
426
+ return unless providers.is_a?(MywayConfig::ConfigSection)
427
+ %i[openai anthropic gemini azure ollama huggingface openrouter bedrock deepseek].each do |provider|
428
+ value = providers[provider]
429
+ providers[provider] = MywayConfig::ConfigSection.new(value) if value.is_a?(Hash)
563
430
  end
564
431
  end
565
432
 
433
+ def coerce_database_integers
434
+ return unless database
435
+ database.port = database.port.to_i if database.port && !database.port.is_a?(Integer)
436
+ database.pool_size = database.pool_size.to_i if database.pool_size && !database.pool_size.is_a?(Integer)
437
+ database.timeout = database.timeout.to_i if database.timeout && !database.timeout.is_a?(Integer)
438
+ end
439
+
440
+ def apply_provider_config(config, provider)
441
+ handler = :"apply_#{provider}_provider_config"
442
+ send(handler, config) if respond_to?(handler, true)
443
+ end
444
+
445
+ def apply_openai_provider_config(config)
446
+ config.openai_api_key = openai_api_key if openai_api_key
447
+ config.openai_organization = openai_organization if openai_organization && config.respond_to?(:openai_organization=)
448
+ config.openai_project = openai_project if openai_project && config.respond_to?(:openai_project=)
449
+ end
450
+
451
+ def apply_anthropic_provider_config(config)
452
+ config.anthropic_api_key = anthropic_api_key if anthropic_api_key
453
+ end
454
+
455
+ def apply_gemini_provider_config(config)
456
+ config.gemini_api_key = gemini_api_key if gemini_api_key
457
+ end
458
+
459
+ def apply_azure_provider_config(config)
460
+ config.azure_api_key = azure_api_key if azure_api_key && config.respond_to?(:azure_api_key=)
461
+ config.azure_endpoint = azure_endpoint if azure_endpoint && config.respond_to?(:azure_endpoint=)
462
+ config.azure_api_version = azure_api_version if azure_api_version && config.respond_to?(:azure_api_version=)
463
+ end
464
+
465
+ def apply_ollama_provider_config(config)
466
+ base = ollama_url.sub(%r{/+$}, '')
467
+ config.ollama_api_base = base.end_with?('/v1') ? base : "#{base}/v1"
468
+ end
469
+
470
+ def apply_huggingface_provider_config(config)
471
+ config.huggingface_api_key = huggingface_api_key if huggingface_api_key && config.respond_to?(:huggingface_api_key=)
472
+ end
473
+
474
+ def apply_openrouter_provider_config(config)
475
+ config.openrouter_api_key = openrouter_api_key if openrouter_api_key && config.respond_to?(:openrouter_api_key=)
476
+ end
477
+
478
+ def apply_bedrock_provider_config(config)
479
+ config.bedrock_api_key = bedrock_access_key if bedrock_access_key && config.respond_to?(:bedrock_api_key=)
480
+ config.bedrock_secret_key = bedrock_secret_key if bedrock_secret_key && config.respond_to?(:bedrock_secret_key=)
481
+ config.bedrock_region = bedrock_region if bedrock_region && config.respond_to?(:bedrock_region=)
482
+ end
483
+
484
+ def apply_deepseek_provider_config(config)
485
+ config.deepseek_api_key = deepseek_api_key if deepseek_api_key && config.respond_to?(:deepseek_api_key=)
486
+ end
487
+
566
488
  # ==========================================================================
567
489
  # Setup Defaults Callback
568
490
  # ==========================================================================
@@ -573,7 +495,7 @@ class HTM
573
495
  @embedding_generator ||= build_default_embedding_generator
574
496
  @tag_extractor ||= build_default_tag_extractor
575
497
  @proposition_extractor ||= build_default_proposition_extractor
576
- @token_counter ||= build_default_token_counter
498
+ @token_counter = build_default_token_counter if @token_counter.nil?
577
499
  end
578
500
 
579
501
  def detect_job_backend
@@ -586,7 +508,5 @@ class HTM
586
508
  end
587
509
  end
588
510
 
589
- # Register custom loaders after Config class is defined
590
- # Order matters: defaults (lowest priority) -> XDG -> project config -> ENV (highest)
591
- require_relative 'loaders/defaults_loader'
592
- require_relative 'loaders/xdg_config_loader'
511
+ # myway_config provides DefaultsLoader and XdgConfigLoader automatically
512
+ # Loaders are registered when MywayConfig.setup! is called (happens on require)