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
@@ -1,3 +1,5 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
1
3
  <div class="space-y-6">
2
4
  <!-- Back link -->
3
5
  <div>
@@ -41,7 +43,7 @@
41
43
  </svg>
42
44
  Restore
43
45
  <% end %>
44
- <%= button_to memory_path(@memory.id, permanent: true), method: :delete, class: 'inline-flex items-center gap-2 rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500 transition-colors', data: { turbo_confirm: 'Permanently delete this memory? This cannot be undone!' } do %>
46
+ <%= button_to memory_path(@memory.id, permanent: true), method: :delete, data: { confirm: 'Permanently delete this memory? This cannot be undone!' }, class: 'inline-flex items-center gap-2 rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500 transition-colors' do %>
45
47
  <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
46
48
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
47
49
  </svg>
@@ -54,7 +56,7 @@
54
56
  </svg>
55
57
  Edit
56
58
  <% end %>
57
- <%= button_to memory_path(@memory.id), method: :delete, class: 'inline-flex items-center gap-2 rounded-md bg-red-600/80 px-3 py-2 text-sm font-medium text-white hover:bg-red-600 transition-colors', data: { turbo_confirm: 'Move this memory to trash?' } do %>
59
+ <%= button_to memory_path(@memory.id), method: :delete, data: { confirm: 'Move this memory to trash?' }, class: 'inline-flex items-center gap-2 rounded-md bg-red-600/80 px-3 py-2 text-sm font-medium text-white hover:bg-red-600 transition-colors' do %>
58
60
  <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
59
61
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
60
62
  </svg>
@@ -0,0 +1,20 @@
1
+ <% is_user = message.role == 'user' %>
2
+
3
+ <div class="flex <%= is_user ? 'justify-end' : 'justify-start' %>">
4
+ <div class="max-w-[80%] <%= is_user ? 'order-2' : 'order-1' %>">
5
+ <div class="rounded-lg px-4 py-3 <%= is_user ? 'bg-indigo-600 text-white' : 'bg-gray-800 border border-gray-700 text-gray-100' %>">
6
+ <div class="prose prose-sm <%= is_user ? 'prose-invert' : 'prose-gray' %> max-w-none">
7
+ <p class="whitespace-pre-wrap break-words"><%= message.content %></p>
8
+ </div>
9
+ </div>
10
+ <div class="flex items-center gap-2 mt-1 <%= is_user ? 'justify-end' : 'justify-start' %>">
11
+ <span class="text-xs text-gray-500">
12
+ <%= message.role.capitalize %>
13
+ </span>
14
+ <span class="text-xs text-gray-600">&bull;</span>
15
+ <time class="text-xs text-gray-500 local-time" datetime="<%= message.created_at.utc.iso8601 %>">
16
+ <%= message.created_at.strftime('%H:%M') %>
17
+ </time>
18
+ </div>
19
+ </div>
20
+ </div>
@@ -1,3 +1,5 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
1
3
  <div class="space-y-6">
2
4
  <!-- Header -->
3
5
  <div class="flex items-center justify-between">
@@ -1,3 +1,5 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
1
3
  <div class="space-y-6">
2
4
  <!-- Back link -->
3
5
  <div>
@@ -1,3 +1,5 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
1
3
  <div class="space-y-6">
2
4
  <!-- Back link -->
3
5
  <div>
@@ -1,3 +1,6 @@
1
+ <%# Disable auto-refresh on search page - searches take time %>
2
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
3
+
1
4
  <div class="space-y-6">
2
5
  <!-- Header -->
3
6
  <div>
@@ -7,7 +10,7 @@
7
10
 
8
11
  <!-- Search Form -->
9
12
  <div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
10
- <%= form_with url: search_path, method: :get, class: 'space-y-4', data: { turbo: false } do %>
13
+ <%= form_with url: search_path, method: :get, class: 'space-y-4', html: { id: 'search-form' } do %>
11
14
  <div>
12
15
  <label for="query" class="block text-sm font-medium text-gray-300 mb-2">Search Query</label>
13
16
  <div class="flex gap-4">
@@ -19,7 +22,13 @@
19
22
  placeholder="Enter your search query..."
20
23
  class="flex-1 rounded-md bg-gray-700 border-gray-600 text-white placeholder-gray-400 focus:ring-indigo-500 focus:border-indigo-500"
21
24
  />
22
- <%= submit_tag 'Search', class: 'rounded-md bg-indigo-600 px-6 py-2 text-sm font-medium text-white hover:bg-indigo-500 cursor-pointer transition-colors' %>
25
+ <button type="submit" id="search-btn" class="rounded-md bg-indigo-600 px-6 py-2 text-sm font-medium text-white hover:bg-indigo-500 cursor-pointer transition-colors inline-flex items-center gap-2">
26
+ <svg id="search-spinner" class="hidden animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
27
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
28
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
29
+ </svg>
30
+ <span id="search-btn-text">Search</span>
31
+ </button>
23
32
  </div>
24
33
  </div>
25
34
 
@@ -114,10 +123,23 @@
114
123
  <ul class="space-y-3">
115
124
  <% results.each_with_index do |memory, i| %>
116
125
  <li class="text-sm">
117
- <span class="text-gray-500 font-mono text-xs"><%= i + 1 %>.</span>
118
- <%= link_to memory_path(memory['id']), class: 'text-gray-300 hover:text-white line-clamp-2' do %>
119
- <%= truncate(memory['content'], length: 80) %>
120
- <% end %>
126
+ <div class="flex items-start gap-2">
127
+ <span class="text-gray-500 font-mono text-xs flex-shrink-0"><%= i + 1 %>.</span>
128
+ <div class="min-w-0">
129
+ <%= link_to memory_path(memory['id']), class: 'text-gray-300 hover:text-white line-clamp-2 block' do %>
130
+ <%= truncate(memory['content'], length: 80) %>
131
+ <% end %>
132
+ <div class="mt-1 text-xs">
133
+ <% if strategy == :hybrid && memory['rrf_score'] %>
134
+ <span class="text-purple-400"><%= (memory['rrf_score'].to_f * 100).round(1) %>%</span>
135
+ <% elsif strategy == :vector && memory['similarity'] %>
136
+ <span class="text-blue-400"><%= (memory['similarity'].to_f * 100).round(1) %>%</span>
137
+ <% elsif strategy == :fulltext && memory['rank'] %>
138
+ <span class="text-green-400"><%= memory['rank'].to_f.round(2) %></span>
139
+ <% end %>
140
+ </div>
141
+ </div>
142
+ </div>
121
143
  </li>
122
144
  <% end %>
123
145
  </ul>
@@ -160,8 +182,17 @@
160
182
  <div class="mt-2 flex items-center gap-4 text-xs text-gray-500">
161
183
  <span class="font-mono text-gray-600">#<%= memory['id'] %></span>
162
184
  <span><%= time_ago_in_words(memory['created_at'].is_a?(Time) ? memory['created_at'] : Time.parse(memory['created_at'].to_s)) %> ago</span>
163
- <% if memory['rank'] %>
164
- <span>Rank: <%= memory['rank'].to_f.round(4) %></span>
185
+ <%# Display score based on search strategy %>
186
+ <% case @selected_strategy %>
187
+ <% when :hybrid %>
188
+ <span class="text-purple-400">RRF: <%= (memory['rrf_score'].to_f * 100).round(2) %>%</span>
189
+ <% if memory['sources'].is_a?(Array) && memory['sources'].any? %>
190
+ <span class="text-gray-600">via <%= memory['sources'].join(', ') %></span>
191
+ <% end %>
192
+ <% when :vector %>
193
+ <span class="text-blue-400">Similarity: <%= (memory['similarity'].to_f * 100).round(2) %>%</span>
194
+ <% when :fulltext %>
195
+ <span class="text-green-400">Score: <%= memory['rank'].to_f.round(4) %></span>
165
196
  <% end %>
166
197
  </div>
167
198
  </div>
@@ -182,3 +213,23 @@
182
213
  <% end %>
183
214
  <% end %>
184
215
  </div>
216
+
217
+ <script>
218
+ document.addEventListener('DOMContentLoaded', () => {
219
+ const form = document.getElementById('search-form');
220
+ const btn = document.getElementById('search-btn');
221
+ const spinner = document.getElementById('search-spinner');
222
+ const btnText = document.getElementById('search-btn-text');
223
+
224
+ if (form && btn) {
225
+ form.addEventListener('submit', () => {
226
+ // Show spinner and disable button
227
+ spinner.classList.remove('hidden');
228
+ btnText.textContent = 'Searching...';
229
+ btn.disabled = true;
230
+ btn.classList.add('opacity-75', 'cursor-wait');
231
+ btn.classList.remove('hover:bg-indigo-500');
232
+ });
233
+ }
234
+ });
235
+ </script>
@@ -1,3 +1,9 @@
1
+ <%
2
+ # Determine current section
3
+ in_chat_section = controller_name.in?(['chats', 'messages'])
4
+ in_htm_section = controller_name.in?(['dashboard', 'memories', 'tags', 'robots', 'search', 'files'])
5
+ %>
6
+
1
7
  <nav class="bg-gray-800 border-b border-gray-700">
2
8
  <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
3
9
  <div class="flex h-16 items-center justify-between">
@@ -7,46 +13,86 @@
7
13
  <svg class="h-8 w-8 text-indigo-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
8
14
  <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
9
15
  </svg>
10
- <span class="text-xl font-bold text-white">HTM Explorer</span>
16
+ <span class="text-xl font-bold text-white">HTM Demo</span>
11
17
  <% end %>
12
18
  </div>
13
- <div class="hidden md:block">
14
- <div class="ml-10 flex items-baseline space-x-4">
15
- <%= link_to 'Dashboard', root_path,
16
- class: "rounded-md px-3 py-2 text-sm font-medium #{current_page?(root_path) ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
17
- <%= link_to 'Memories', memories_path,
18
- class: "rounded-md px-3 py-2 text-sm font-medium #{controller_name == 'memories' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
19
- <%= link_to 'Tags', tags_path,
20
- class: "rounded-md px-3 py-2 text-sm font-medium #{controller_name == 'tags' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
21
- <%= link_to 'Robots', robots_path,
22
- class: "rounded-md px-3 py-2 text-sm font-medium #{controller_name == 'robots' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
23
- <%= link_to 'Search', search_path,
24
- class: "rounded-md px-3 py-2 text-sm font-medium #{controller_name == 'search' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
25
- <%= link_to 'Files', files_path,
26
- class: "rounded-md px-3 py-2 text-sm font-medium #{controller_name == 'files' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
19
+
20
+ <% if in_chat_section %>
21
+ <!-- Chat section navigation -->
22
+ <div class="hidden md:block">
23
+ <div class="ml-10 flex items-baseline space-x-4">
24
+ <span class="rounded-full bg-indigo-900/50 border border-indigo-500/30 px-3 py-1 text-xs font-medium text-indigo-400">Chat</span>
25
+ <%= link_to 'All Chats', app_root_path,
26
+ class: "rounded-md px-3 py-2 text-sm font-medium #{controller_name == 'chats' && action_name == 'index' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
27
+ <%= link_to 'HTM Management', htm_root_path,
28
+ class: "rounded-md px-3 py-2 text-sm font-medium text-gray-400 hover:bg-gray-700 hover:text-white" %>
29
+ </div>
27
30
  </div>
28
- </div>
31
+ <% elsif in_htm_section %>
32
+ <!-- HTM section navigation -->
33
+ <div class="hidden md:block">
34
+ <div class="ml-10 flex items-baseline space-x-4">
35
+ <span class="rounded-full bg-purple-900/50 border border-purple-500/30 px-3 py-1 text-xs font-medium text-purple-400">HTM</span>
36
+ <%= link_to 'Dashboard', htm_root_path,
37
+ class: "rounded-md px-3 py-2 text-sm font-medium #{controller_name == 'dashboard' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
38
+ <%= link_to 'Memories', memories_path,
39
+ class: "rounded-md px-3 py-2 text-sm font-medium #{controller_name == 'memories' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
40
+ <%= link_to 'Tags', tags_path,
41
+ class: "rounded-md px-3 py-2 text-sm font-medium #{controller_name == 'tags' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
42
+ <%= link_to 'Robots', robots_path,
43
+ class: "rounded-md px-3 py-2 text-sm font-medium #{controller_name == 'robots' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
44
+ <%= link_to 'Search', search_path,
45
+ class: "rounded-md px-3 py-2 text-sm font-medium #{controller_name == 'search' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
46
+ <%= link_to 'Files', files_path,
47
+ class: "rounded-md px-3 py-2 text-sm font-medium #{controller_name == 'files' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
48
+ <%= link_to 'Chat', app_root_path,
49
+ class: "rounded-md px-3 py-2 text-sm font-medium text-gray-400 hover:bg-gray-700 hover:text-white" %>
50
+ </div>
51
+ </div>
52
+ <% else %>
53
+ <!-- Home page - show both section links -->
54
+ <div class="hidden md:block">
55
+ <div class="ml-10 flex items-baseline space-x-4">
56
+ <%= link_to 'Chat', app_root_path,
57
+ class: "rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
58
+ <%= link_to 'HTM Management', htm_root_path,
59
+ class: "rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
60
+ </div>
61
+ </div>
62
+ <% end %>
29
63
  </div>
30
- <div class="flex items-center gap-4">
31
- <div class="flex items-center gap-2 text-sm text-gray-400">
32
- <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
33
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
34
- </svg>
35
- <span>Robot: <strong class="text-indigo-400"><%= current_robot_name %></strong></span>
64
+
65
+ <% if in_htm_section %>
66
+ <div class="flex items-center gap-4">
67
+ <div class="flex items-center gap-2 text-sm text-gray-400">
68
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
69
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
70
+ </svg>
71
+ <span>Robot: <strong class="text-indigo-400"><%= current_robot_name %></strong></span>
72
+ </div>
36
73
  </div>
37
- </div>
74
+ <% end %>
38
75
  </div>
39
76
  </div>
40
77
 
41
78
  <!-- Mobile menu -->
42
79
  <div class="md:hidden" data-controller="mobile-menu">
43
80
  <div class="space-y-1 px-2 pb-3 pt-2 sm:px-3">
44
- <%= link_to 'Dashboard', root_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
45
- <%= link_to 'Memories', memories_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
46
- <%= link_to 'Tags', tags_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
47
- <%= link_to 'Robots', robots_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
48
- <%= link_to 'Search', search_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
49
- <%= link_to 'Files', files_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
81
+ <% if in_chat_section %>
82
+ <%= link_to 'All Chats', app_root_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
83
+ <%= link_to 'HTM Management', htm_root_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-400 hover:bg-gray-700 hover:text-white" %>
84
+ <% elsif in_htm_section %>
85
+ <%= link_to 'Dashboard', htm_root_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
86
+ <%= link_to 'Memories', memories_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
87
+ <%= link_to 'Tags', tags_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
88
+ <%= link_to 'Robots', robots_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
89
+ <%= link_to 'Search', search_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
90
+ <%= link_to 'Files', files_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
91
+ <%= link_to 'Chat', app_root_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-400 hover:bg-gray-700 hover:text-white" %>
92
+ <% else %>
93
+ <%= link_to 'Chat', app_root_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
94
+ <%= link_to 'HTM Management', htm_root_path, class: "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" %>
95
+ <% end %>
50
96
  </div>
51
97
  </div>
52
98
  </nav>
@@ -1,3 +1,5 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
1
3
  <div class="space-y-6">
2
4
  <!-- Header -->
3
5
  <div class="flex items-center justify-between">
@@ -1,3 +1,5 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
1
3
  <div class="space-y-6">
2
4
  <!-- Back link -->
3
5
  <div>
@@ -34,7 +36,7 @@
34
36
  <% prefix = parts[0..i].join(':') %>
35
37
  <% matching_tag = HTM::Models::Tag.first(name: prefix) %>
36
38
  <% if matching_tag %>
37
- <%= link_to part, tag_path(matching_tag), class: "#{i == parts.length - 1 ? 'text-indigo-400' : 'text-gray-400 hover:text-white'}" %>
39
+ <%= link_to part, tag_path(matching_tag.id), class: "#{i == parts.length - 1 ? 'text-indigo-400' : 'text-gray-400 hover:text-white'}" %>
38
40
  <% else %>
39
41
  <span class="text-gray-500"><%= part %></span>
40
42
  <% end %>
@@ -14,7 +14,7 @@ require 'htm'
14
14
 
15
15
  module HtmRailsExample
16
16
  class Application < Rails::Application
17
- config.load_defaults 7.1
17
+ config.load_defaults 8.0
18
18
 
19
19
  # Full-stack Rails app (not API-only)
20
20
  config.api_only = false
@@ -1,15 +1,19 @@
1
- # Rails app database (not used - HTM uses its own database)
2
- # This is required for Rails to boot but can be sqlite/memory
1
+ # Rails app database for RubyLLM chat persistence
2
+ # HTM uses its own database (htm_examples)
3
3
 
4
4
  default: &default
5
5
  adapter: postgresql
6
6
  encoding: unicode
7
- pool: 5
7
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
8
8
 
9
9
  development:
10
10
  <<: *default
11
- database: htm_rails_example_dev
11
+ database: chatbot_development
12
12
 
13
13
  test:
14
14
  <<: *default
15
- database: htm_rails_example_test
15
+ database: chatbot_test
16
+
17
+ examples:
18
+ <<: *default
19
+ database: chatbot_examples
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  pin "application"
4
- pin "@hotwired/turbo-rails", to: "turbo.min.js"
4
+ # Turbo removed - causes issues with CDN-based setup, using vanilla JS instead
5
5
  pin "@hotwired/stimulus", to: "stimulus.min.js"
6
6
  pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
7
7
  pin_all_from "app/javascript/controllers", under: "controllers"
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Override the examples_helper.rb :inline setting for the Rails app
4
- # Use fiber-based async jobs for better responsiveness
3
+ # Configure HTM for the Rails chatbot app
5
4
  HTM.configure do |config|
5
+ # Use the examples database which contains the knowledge base
6
+ # This overrides HTM_ENV=development to use the populated database
7
+ config.database.url = ENV.fetch('HTM_CHATBOT_DATABASE_URL', 'postgresql://localhost:5432/htm_examples')
8
+
9
+ # Use fiber-based async jobs for better responsiveness
6
10
  config.job.backend = :fiber
7
11
  end
12
+
13
+ Rails.logger.info "HTM initialized for Rails application"
14
+ Rails.logger.info "HTM database: #{HTM.configuration.database_url}"
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_llm'
4
+
5
+ RubyLLM.configure do |config|
6
+ # Logging - use INFO level in production, DEBUG for troubleshooting
7
+ config.logger = Rails.logger
8
+ config.log_level = Rails.env.development? ? Logger::INFO : Logger::WARN
9
+
10
+ # Enable new acts_as API (required for acts_as_chat)
11
+ config.use_new_acts_as = true
12
+
13
+ # Model registry class for database storage
14
+ config.model_registry_class = 'Model'
15
+
16
+ # Default chat model
17
+ config.default_model = ENV.fetch('CHAT_MODEL', 'gemma3:latest')
18
+
19
+ # Cloud provider API keys
20
+ config.openai_api_key = ENV['OPENAI_API_KEY']
21
+ config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
22
+ config.xai_api_key = ENV['XAI_API_KEY']
23
+
24
+ # Ollama (local provider) - requires /v1 suffix for OpenAI-compatible API
25
+ ollama_base = ENV.fetch('OLLAMA_URL', 'http://localhost:11434')
26
+ config.ollama_api_base = ollama_base.end_with?('/v1') ? ollama_base : "#{ollama_base}/v1"
27
+ end
28
+
29
+ # Include ActiveRecord support directly since railtie hook doesn't always fire correctly
30
+ Rails.application.config.to_prepare do
31
+ require 'ruby_llm/active_record/acts_as'
32
+ ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAs unless ActiveRecord::Base.respond_to?(:acts_as_chat)
33
+ end
@@ -1,38 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Rails.application.routes.draw do
4
- root 'dashboard#index'
4
+ # Landing page
5
+ root 'home#index'
5
6
 
6
- resources :memories do
7
- collection do
8
- get :deleted
9
- end
10
- member do
11
- post :restore
7
+ # Chatbot section
8
+ scope '/app' do
9
+ get '/', to: 'chats#index', as: :app_root
10
+ resources :chats, only: [:index, :show, :create, :update, :destroy] do
11
+ member do
12
+ patch :update_htm_settings
13
+ end
14
+ resources :messages, only: [:create]
12
15
  end
16
+ get '/models', to: 'chats#models', as: :provider_models
13
17
  end
14
18
 
15
- resources :tags, only: [:index, :show]
19
+ # HTM Management section (existing functionality)
20
+ scope '/htm' do
21
+ get '/', to: 'dashboard#index', as: :htm_root
16
22
 
17
- resources :robots, only: [:index, :show, :new, :create] do
18
- member do
19
- post :switch
23
+ resources :memories do
24
+ collection do
25
+ get :deleted
26
+ end
27
+ member do
28
+ post :restore
29
+ end
20
30
  end
21
- end
22
31
 
23
- resource :search, only: [:index], controller: 'search' do
24
- get '/', action: :index
25
- end
32
+ resources :tags, only: [:index, :show]
26
33
 
27
- resources :files, only: [:index, :show, :new, :create, :destroy] do
28
- collection do
29
- post :load_directory
30
- post :upload
31
- post :upload_directory
32
- post :sync_all
34
+ resources :robots, only: [:index, :show, :new, :create] do
35
+ member do
36
+ post :switch
37
+ end
33
38
  end
34
- member do
35
- post :sync
39
+
40
+ get '/search', to: 'search#index', as: :search
41
+
42
+ resources :files, only: [:index, :show, :new, :create, :destroy] do
43
+ collection do
44
+ post :load_directory
45
+ post :upload
46
+ post :upload_directory
47
+ post :sync_all
48
+ end
49
+ member do
50
+ post :sync
51
+ end
36
52
  end
37
53
  end
38
54
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRubyLlmTables < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :chats do |t|
6
+ t.string :model_id
7
+ t.text :instructions
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ create_table :messages do |t|
13
+ t.references :chat, null: false, foreign_key: true
14
+ t.string :role, null: false
15
+ t.text :content
16
+ t.string :model_id
17
+ t.integer :input_tokens
18
+ t.integer :output_tokens
19
+
20
+ t.timestamps
21
+ end
22
+
23
+ create_table :tool_calls do |t|
24
+ t.references :message, null: false, foreign_key: true
25
+ t.string :tool_call_id, null: false
26
+ t.string :name, null: false
27
+ t.jsonb :arguments, default: {}
28
+
29
+ t.timestamps
30
+ end
31
+
32
+ add_index :tool_calls, :tool_call_id
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateModelsTable < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :models do |t|
6
+ t.string :model_id, null: false
7
+ t.string :name, null: false
8
+ t.string :provider, null: false
9
+ t.string :family
10
+ t.datetime :model_created_at
11
+ t.integer :context_window
12
+ t.integer :max_output_tokens
13
+ t.date :knowledge_cutoff
14
+ t.jsonb :modalities, default: {}
15
+ t.jsonb :capabilities, default: []
16
+ t.jsonb :pricing, default: {}
17
+ t.jsonb :metadata, default: {}
18
+
19
+ t.timestamps
20
+ end
21
+
22
+ add_index :models, [:provider, :model_id], unique: true
23
+ add_index :models, :provider
24
+ add_index :models, :family
25
+ add_index :models, :capabilities, using: :gin
26
+ add_index :models, :modalities, using: :gin
27
+ end
28
+ end
@@ -0,0 +1,67 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema[8.1].define(version: 2025_01_24_000002) do
14
+ # These are extensions that must be enabled in order to support this database
15
+ enable_extension "pg_catalog.plpgsql"
16
+
17
+ create_table "chats", force: :cascade do |t|
18
+ t.datetime "created_at", null: false
19
+ t.text "instructions"
20
+ t.string "model_id"
21
+ t.datetime "updated_at", null: false
22
+ end
23
+
24
+ create_table "messages", force: :cascade do |t|
25
+ t.bigint "chat_id", null: false
26
+ t.text "content"
27
+ t.datetime "created_at", null: false
28
+ t.integer "input_tokens"
29
+ t.string "model_id"
30
+ t.integer "output_tokens"
31
+ t.string "role", null: false
32
+ t.datetime "updated_at", null: false
33
+ t.index ["chat_id"], name: "index_messages_on_chat_id"
34
+ end
35
+
36
+ create_table "models", force: :cascade do |t|
37
+ t.integer "context_window"
38
+ t.datetime "created_at", null: false
39
+ t.string "family"
40
+ t.decimal "input_price_per_million", precision: 10, scale: 4
41
+ t.integer "max_tokens"
42
+ t.jsonb "metadata", default: {}
43
+ t.string "model_id", null: false
44
+ t.string "name"
45
+ t.decimal "output_price_per_million", precision: 10, scale: 4
46
+ t.string "provider"
47
+ t.boolean "supports_functions", default: false
48
+ t.boolean "supports_vision", default: false
49
+ t.datetime "updated_at", null: false
50
+ t.index ["model_id"], name: "index_models_on_model_id", unique: true
51
+ t.index ["provider"], name: "index_models_on_provider"
52
+ end
53
+
54
+ create_table "tool_calls", force: :cascade do |t|
55
+ t.jsonb "arguments", default: {}
56
+ t.datetime "created_at", null: false
57
+ t.bigint "message_id", null: false
58
+ t.string "name", null: false
59
+ t.string "tool_call_id", null: false
60
+ t.datetime "updated_at", null: false
61
+ t.index ["message_id"], name: "index_tool_calls_on_message_id"
62
+ t.index ["tool_call_id"], name: "index_tool_calls_on_tool_call_id"
63
+ end
64
+
65
+ add_foreign_key "messages", "chats"
66
+ add_foreign_key "tool_calls", "messages"
67
+ end