htm 0.0.15 → 0.0.17
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/.envrc +1 -0
- data/CHANGELOG.md +67 -0
- data/README.md +97 -1592
- data/bin/htm_mcp +31 -0
- data/config/database.yml +7 -4
- data/docs/getting-started/installation.md +31 -11
- data/docs/guides/mcp-server.md +456 -21
- data/docs/multi_framework_support.md +2 -2
- data/examples/mcp_client.rb +2 -2
- data/examples/rails_app/.gitignore +2 -0
- data/examples/rails_app/Gemfile +22 -0
- data/examples/rails_app/Gemfile.lock +438 -0
- data/examples/rails_app/Procfile.dev +1 -0
- data/examples/rails_app/README.md +98 -0
- data/examples/rails_app/Rakefile +5 -0
- data/examples/rails_app/app/assets/stylesheets/application.css +83 -0
- data/examples/rails_app/app/assets/stylesheets/inter-font.css +6 -0
- data/examples/rails_app/app/controllers/application_controller.rb +19 -0
- data/examples/rails_app/app/controllers/dashboard_controller.rb +27 -0
- data/examples/rails_app/app/controllers/files_controller.rb +205 -0
- data/examples/rails_app/app/controllers/memories_controller.rb +102 -0
- data/examples/rails_app/app/controllers/robots_controller.rb +44 -0
- data/examples/rails_app/app/controllers/search_controller.rb +46 -0
- data/examples/rails_app/app/controllers/tags_controller.rb +30 -0
- data/examples/rails_app/app/javascript/application.js +4 -0
- data/examples/rails_app/app/javascript/controllers/application.js +9 -0
- data/examples/rails_app/app/javascript/controllers/index.js +6 -0
- data/examples/rails_app/app/views/dashboard/index.html.erb +123 -0
- data/examples/rails_app/app/views/files/index.html.erb +108 -0
- data/examples/rails_app/app/views/files/new.html.erb +321 -0
- data/examples/rails_app/app/views/files/show.html.erb +130 -0
- data/examples/rails_app/app/views/layouts/application.html.erb +124 -0
- data/examples/rails_app/app/views/memories/_memory_card.html.erb +51 -0
- data/examples/rails_app/app/views/memories/deleted.html.erb +62 -0
- data/examples/rails_app/app/views/memories/edit.html.erb +35 -0
- data/examples/rails_app/app/views/memories/index.html.erb +81 -0
- data/examples/rails_app/app/views/memories/new.html.erb +71 -0
- data/examples/rails_app/app/views/memories/show.html.erb +126 -0
- data/examples/rails_app/app/views/robots/index.html.erb +106 -0
- data/examples/rails_app/app/views/robots/new.html.erb +36 -0
- data/examples/rails_app/app/views/robots/show.html.erb +79 -0
- data/examples/rails_app/app/views/search/index.html.erb +184 -0
- data/examples/rails_app/app/views/shared/_navbar.html.erb +52 -0
- data/examples/rails_app/app/views/shared/_stat_card.html.erb +52 -0
- data/examples/rails_app/app/views/tags/index.html.erb +131 -0
- data/examples/rails_app/app/views/tags/show.html.erb +67 -0
- data/examples/rails_app/bin/dev +8 -0
- data/examples/rails_app/bin/rails +4 -0
- data/examples/rails_app/bin/rake +4 -0
- data/examples/rails_app/config/application.rb +33 -0
- data/examples/rails_app/config/boot.rb +5 -0
- data/examples/rails_app/config/database.yml +15 -0
- data/examples/rails_app/config/environment.rb +5 -0
- data/examples/rails_app/config/importmap.rb +7 -0
- data/examples/rails_app/config/routes.rb +38 -0
- data/examples/rails_app/config/tailwind.config.js +35 -0
- data/examples/rails_app/config.ru +5 -0
- data/examples/rails_app/log/.keep +0 -0
- data/examples/rails_app/tmp/local_secret.txt +1 -0
- data/lib/htm/active_record_config.rb +2 -5
- data/lib/htm/configuration.rb +35 -2
- data/lib/htm/database.rb +3 -6
- data/lib/htm/mcp/cli.rb +333 -0
- data/lib/htm/mcp/group_tools.rb +476 -0
- data/lib/htm/mcp/resources.rb +89 -0
- data/lib/htm/mcp/server.rb +98 -0
- data/lib/htm/mcp/tools.rb +488 -0
- data/lib/htm/models/file_source.rb +5 -3
- data/lib/htm/railtie.rb +0 -4
- data/lib/htm/tasks.rb +7 -4
- data/lib/htm/version.rb +1 -1
- data/lib/tasks/htm.rake +6 -9
- metadata +59 -4
- data/bin/htm_mcp.rb +0 -621
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ApplicationController < ActionController::Base
|
|
4
|
+
# HTM instance for the current request
|
|
5
|
+
def htm
|
|
6
|
+
@htm ||= HTM.new(robot_name: current_robot_name)
|
|
7
|
+
end
|
|
8
|
+
helper_method :htm
|
|
9
|
+
|
|
10
|
+
# Allow switching robots via session
|
|
11
|
+
def current_robot_name
|
|
12
|
+
session[:robot_name] || 'explorer'
|
|
13
|
+
end
|
|
14
|
+
helper_method :current_robot_name
|
|
15
|
+
|
|
16
|
+
def current_robot_name=(name)
|
|
17
|
+
session[:robot_name] = name
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class DashboardController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
# Note: HTM::Models::Node has a default_scope that excludes deleted nodes
|
|
6
|
+
# so we don't need to call .active explicitly
|
|
7
|
+
@stats = {
|
|
8
|
+
total_nodes: HTM::Models::Node.count,
|
|
9
|
+
nodes_with_embeddings: HTM::Models::Node.with_embeddings.count,
|
|
10
|
+
deleted_nodes: HTM::Models::Node.deleted.count,
|
|
11
|
+
total_tags: HTM::Models::Tag.count,
|
|
12
|
+
total_robots: HTM::Models::Robot.count,
|
|
13
|
+
total_file_sources: HTM::Models::FileSource.count
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@recent_memories = HTM::Models::Node.recent.limit(5)
|
|
17
|
+
|
|
18
|
+
@top_tags = HTM::Models::Tag
|
|
19
|
+
.joins(:nodes)
|
|
20
|
+
.group('tags.id')
|
|
21
|
+
.order('COUNT(nodes.id) DESC')
|
|
22
|
+
.limit(10)
|
|
23
|
+
.select('tags.*, COUNT(nodes.id) as node_count')
|
|
24
|
+
|
|
25
|
+
@robots = HTM::Models::Robot.order(created_at: :desc).limit(5)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class FilesController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
@file_sources = HTM::Models::FileSource.order(updated_at: :desc)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
@file_source = HTM::Models::FileSource.find(params[:id])
|
|
10
|
+
@chunks = @file_source.chunks.order(:id)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create
|
|
17
|
+
path = params[:path]&.strip
|
|
18
|
+
|
|
19
|
+
if path.blank?
|
|
20
|
+
flash[:alert] = 'File path is required'
|
|
21
|
+
redirect_to new_file_path
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
unless File.exist?(path)
|
|
26
|
+
flash[:alert] = "File not found: #{path}"
|
|
27
|
+
redirect_to new_file_path
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
force = params[:force] == 'true'
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
result = htm.load_file(path, force: force)
|
|
35
|
+
flash[:notice] = "File loaded: #{result[:chunks_created]} chunks created, #{result[:chunks_updated]} updated, #{result[:chunks_deleted]} deleted"
|
|
36
|
+
redirect_to file_path(result[:file_source_id])
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
flash[:alert] = "Error loading file: #{e.message}"
|
|
39
|
+
redirect_to new_file_path
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def load_directory
|
|
44
|
+
path = params[:path]&.strip
|
|
45
|
+
pattern = params[:pattern] || '**/*.md'
|
|
46
|
+
|
|
47
|
+
if path.blank?
|
|
48
|
+
flash[:alert] = 'Directory path is required'
|
|
49
|
+
redirect_to new_file_path
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless Dir.exist?(path)
|
|
54
|
+
flash[:alert] = "Directory not found: #{path}"
|
|
55
|
+
redirect_to new_file_path
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
results = htm.load_directory(path, pattern: pattern)
|
|
61
|
+
total_chunks = results.sum { |r| r[:chunks_created] }
|
|
62
|
+
flash[:notice] = "Loaded #{results.length} files with #{total_chunks} total chunks"
|
|
63
|
+
redirect_to files_path
|
|
64
|
+
rescue StandardError => e
|
|
65
|
+
flash[:alert] = "Error loading directory: #{e.message}"
|
|
66
|
+
redirect_to new_file_path
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def upload
|
|
71
|
+
files = params[:files]
|
|
72
|
+
force = params[:force] == 'true'
|
|
73
|
+
|
|
74
|
+
if files.blank?
|
|
75
|
+
flash[:alert] = 'Please select at least one file'
|
|
76
|
+
redirect_to new_file_path
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
results = process_uploaded_files(files, force: force)
|
|
81
|
+
|
|
82
|
+
if results[:errors].any?
|
|
83
|
+
flash[:alert] = "Loaded #{results[:success_count]} files with #{results[:errors].length} errors: #{results[:errors].first}"
|
|
84
|
+
else
|
|
85
|
+
flash[:notice] = "Loaded #{results[:success_count]} files with #{results[:total_chunks]} total chunks"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if results[:last_file_source_id] && results[:success_count] == 1
|
|
89
|
+
redirect_to file_path(results[:last_file_source_id])
|
|
90
|
+
else
|
|
91
|
+
redirect_to files_path
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def upload_directory
|
|
96
|
+
files = params[:files]
|
|
97
|
+
extension = params[:extension]
|
|
98
|
+
|
|
99
|
+
if files.blank?
|
|
100
|
+
flash[:alert] = 'Please select a directory'
|
|
101
|
+
redirect_to new_file_path
|
|
102
|
+
return
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Filter files by extension if specified
|
|
106
|
+
filtered_files = if extension.present?
|
|
107
|
+
files.select { |f| f.original_filename.end_with?(extension) }
|
|
108
|
+
else
|
|
109
|
+
files.select { |f| f.original_filename.match?(/\.(md|markdown|txt)$/i) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if filtered_files.empty?
|
|
113
|
+
flash[:alert] = 'No matching files found in the selected directory'
|
|
114
|
+
redirect_to new_file_path
|
|
115
|
+
return
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
results = process_uploaded_files(filtered_files, force: false)
|
|
119
|
+
|
|
120
|
+
if results[:errors].any?
|
|
121
|
+
flash[:alert] = "Loaded #{results[:success_count]} files with #{results[:errors].length} errors"
|
|
122
|
+
else
|
|
123
|
+
flash[:notice] = "Loaded #{results[:success_count]} files with #{results[:total_chunks]} total chunks"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
redirect_to files_path
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def sync
|
|
130
|
+
@file_source = HTM::Models::FileSource.find(params[:id])
|
|
131
|
+
|
|
132
|
+
begin
|
|
133
|
+
result = htm.load_file(@file_source.file_path, force: true)
|
|
134
|
+
flash[:notice] = "File synced: #{result[:chunks_created]} created, #{result[:chunks_updated]} updated, #{result[:chunks_deleted]} deleted"
|
|
135
|
+
rescue StandardError => e
|
|
136
|
+
flash[:alert] = "Error syncing file: #{e.message}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
redirect_to file_path(@file_source)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def destroy
|
|
143
|
+
@file_source = HTM::Models::FileSource.find(params[:id])
|
|
144
|
+
|
|
145
|
+
begin
|
|
146
|
+
htm.unload_file(@file_source.file_path)
|
|
147
|
+
flash[:notice] = 'File unloaded successfully'
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
flash[:alert] = "Error unloading file: #{e.message}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
redirect_to files_path
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def sync_all
|
|
156
|
+
synced = 0
|
|
157
|
+
errors = 0
|
|
158
|
+
|
|
159
|
+
HTM::Models::FileSource.find_each do |source|
|
|
160
|
+
if source.needs_sync?
|
|
161
|
+
begin
|
|
162
|
+
htm.load_file(source.file_path, force: true)
|
|
163
|
+
synced += 1
|
|
164
|
+
rescue StandardError
|
|
165
|
+
errors += 1
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if errors.zero?
|
|
171
|
+
flash[:notice] = "Synced #{synced} files"
|
|
172
|
+
else
|
|
173
|
+
flash[:alert] = "Synced #{synced} files with #{errors} errors"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
redirect_to files_path
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def process_uploaded_files(files, force: false)
|
|
182
|
+
results = { success_count: 0, total_chunks: 0, errors: [], last_file_source_id: nil }
|
|
183
|
+
|
|
184
|
+
# Create uploads directory if it doesn't exist
|
|
185
|
+
uploads_dir = Rails.root.join('tmp', 'uploads')
|
|
186
|
+
FileUtils.mkdir_p(uploads_dir)
|
|
187
|
+
|
|
188
|
+
files.each do |file|
|
|
189
|
+
# Save uploaded file to temp location
|
|
190
|
+
temp_path = uploads_dir.join(file.original_filename)
|
|
191
|
+
File.open(temp_path, 'wb') { |f| f.write(file.read) }
|
|
192
|
+
|
|
193
|
+
begin
|
|
194
|
+
result = htm.load_file(temp_path.to_s, force: force)
|
|
195
|
+
results[:success_count] += 1
|
|
196
|
+
results[:total_chunks] += result[:chunks_created]
|
|
197
|
+
results[:last_file_source_id] = result[:file_source_id]
|
|
198
|
+
rescue StandardError => e
|
|
199
|
+
results[:errors] << "#{file.original_filename}: #{e.message}"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
results
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class MemoriesController < ApplicationController
|
|
4
|
+
before_action :set_memory, only: [:show, :edit, :update, :destroy, :restore]
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
# Default scope excludes deleted nodes, so no .active needed
|
|
8
|
+
@memories = HTM::Models::Node.includes(:tags)
|
|
9
|
+
.order(created_at: :desc)
|
|
10
|
+
|
|
11
|
+
if params[:tag].present?
|
|
12
|
+
@memories = @memories.joins(:tags).where(tags: { name: params[:tag] })
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if params[:search].present?
|
|
16
|
+
@memories = @memories.where('content ILIKE ?', "%#{params[:search]}%")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Simple pagination without Kaminari
|
|
20
|
+
@page = (params[:page] || 1).to_i
|
|
21
|
+
@per_page = 20
|
|
22
|
+
@total_count = @memories.count
|
|
23
|
+
@total_pages = (@total_count.to_f / @per_page).ceil
|
|
24
|
+
@memories = @memories.offset((@page - 1) * @per_page).limit(@per_page)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def show
|
|
28
|
+
@related = htm.recall(@memory.content, limit: 5, strategy: :vector, raw: true)
|
|
29
|
+
.reject { |m| m['id'] == @memory.id }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def new
|
|
33
|
+
@memory = HTM::Models::Node.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def create
|
|
37
|
+
content = params[:content] || params.dig(:node, :content)
|
|
38
|
+
tags = params[:tags] || params.dig(:node, :tags)
|
|
39
|
+
metadata = params[:metadata] || params.dig(:node, :metadata)
|
|
40
|
+
|
|
41
|
+
if content.blank?
|
|
42
|
+
flash[:alert] = 'Content is required'
|
|
43
|
+
redirect_to new_memory_path
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
tag_array = tags.present? ? tags.split(',').map(&:strip) : []
|
|
48
|
+
metadata_hash = metadata.present? ? JSON.parse(metadata) : {}
|
|
49
|
+
|
|
50
|
+
node_id = htm.remember(content, tags: tag_array, metadata: metadata_hash)
|
|
51
|
+
flash[:notice] = 'Memory stored successfully'
|
|
52
|
+
redirect_to memory_path(node_id)
|
|
53
|
+
rescue JSON::ParserError
|
|
54
|
+
flash[:alert] = 'Invalid metadata JSON format'
|
|
55
|
+
redirect_to new_memory_path
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def edit
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def update
|
|
62
|
+
content = params[:content] || params.dig(:node, :content)
|
|
63
|
+
|
|
64
|
+
if content.blank?
|
|
65
|
+
flash[:alert] = 'Content is required'
|
|
66
|
+
redirect_to edit_memory_path(@memory)
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@memory.update!(content: content)
|
|
71
|
+
flash[:notice] = 'Memory updated successfully'
|
|
72
|
+
redirect_to memory_path(@memory)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def destroy
|
|
76
|
+
soft = params[:permanent] != 'true'
|
|
77
|
+
if soft
|
|
78
|
+
htm.forget(@memory.id)
|
|
79
|
+
flash[:notice] = 'Memory moved to trash. You can restore it later.'
|
|
80
|
+
else
|
|
81
|
+
htm.forget(@memory.id, soft: false, confirm: :confirmed)
|
|
82
|
+
flash[:notice] = 'Memory permanently deleted.'
|
|
83
|
+
end
|
|
84
|
+
redirect_to memories_path
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def restore
|
|
88
|
+
htm.restore(@memory.id)
|
|
89
|
+
flash[:notice] = 'Memory restored successfully.'
|
|
90
|
+
redirect_to memory_path(@memory)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def deleted
|
|
94
|
+
@memories = HTM::Models::Node.deleted.order(deleted_at: :desc)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def set_memory
|
|
100
|
+
@memory = HTM::Models::Node.with_deleted.find(params[:id])
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class RobotsController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
@robots = HTM::Models::Robot.order(created_at: :desc)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
@robot = HTM::Models::Robot.find(params[:id])
|
|
10
|
+
# Default scope already excludes deleted nodes, so no .active needed
|
|
11
|
+
@memory_count = @robot.nodes.count
|
|
12
|
+
@recent_memories = @robot.nodes.order(created_at: :desc).limit(10)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create
|
|
19
|
+
name = params[:name]&.strip
|
|
20
|
+
|
|
21
|
+
if name.blank?
|
|
22
|
+
flash[:alert] = 'Robot name is required'
|
|
23
|
+
redirect_to new_robot_path
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if HTM::Models::Robot.exists?(name: name)
|
|
28
|
+
flash[:alert] = 'A robot with that name already exists'
|
|
29
|
+
redirect_to new_robot_path
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
robot = HTM::Models::Robot.create!(name: name, metadata: {})
|
|
34
|
+
flash[:notice] = "Robot '#{name}' created successfully"
|
|
35
|
+
redirect_to robot_path(robot)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def switch
|
|
39
|
+
robot = HTM::Models::Robot.find(params[:id])
|
|
40
|
+
self.current_robot_name = robot.name
|
|
41
|
+
flash[:notice] = "Switched to robot '#{robot.name}'"
|
|
42
|
+
redirect_to root_path
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class SearchController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
@query = params[:query]
|
|
6
|
+
@strategies = %i[vector fulltext hybrid]
|
|
7
|
+
@selected_strategy = (params[:strategy] || 'hybrid').to_sym
|
|
8
|
+
@limit = (params[:limit] || 10).to_i
|
|
9
|
+
@timeframe = params[:timeframe].presence || 'all time'
|
|
10
|
+
|
|
11
|
+
@results = {}
|
|
12
|
+
@errors = {}
|
|
13
|
+
|
|
14
|
+
return unless @query.present?
|
|
15
|
+
|
|
16
|
+
# Convert "all time" to nil for HTM (no timeframe filter)
|
|
17
|
+
timeframe_param = @timeframe == 'all time' ? nil : @timeframe
|
|
18
|
+
|
|
19
|
+
if params[:compare] == 'true'
|
|
20
|
+
# Compare all strategies
|
|
21
|
+
@strategies.each do |strategy|
|
|
22
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
23
|
+
begin
|
|
24
|
+
@results[strategy] = htm.recall(@query, limit: @limit, strategy: strategy, timeframe: timeframe_param, raw: true)
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
@results[strategy] = []
|
|
27
|
+
@errors[strategy] = e.message
|
|
28
|
+
end
|
|
29
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
30
|
+
@results["#{strategy}_time".to_sym] = ((end_time - start_time) * 1000).round(2)
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
# Single strategy search
|
|
34
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
35
|
+
begin
|
|
36
|
+
@results[@selected_strategy] = htm.recall(@query, limit: @limit, strategy: @selected_strategy, timeframe: timeframe_param, raw: true)
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
@results[@selected_strategy] = []
|
|
39
|
+
@errors[@selected_strategy] = e.message
|
|
40
|
+
flash.now[:alert] = "Search error: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
43
|
+
@results["#{@selected_strategy}_time".to_sym] = ((end_time - start_time) * 1000).round(2)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class TagsController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
@view_type = params[:view] || 'list'
|
|
6
|
+
|
|
7
|
+
case @view_type
|
|
8
|
+
when 'tree'
|
|
9
|
+
@tree_string = HTM::Models::Tag.all.tree_string
|
|
10
|
+
@tree_svg = HTM::Models::Tag.all.tree_svg(title: 'HTM Tag Hierarchy')
|
|
11
|
+
when 'mermaid'
|
|
12
|
+
@tree_mermaid = HTM::Models::Tag.all.tree_mermaid
|
|
13
|
+
else
|
|
14
|
+
@tags = HTM::Models::Tag
|
|
15
|
+
.left_joins(:nodes)
|
|
16
|
+
.group('tags.id')
|
|
17
|
+
.select('tags.*, COUNT(nodes.id) as node_count')
|
|
18
|
+
.order(:name)
|
|
19
|
+
|
|
20
|
+
if params[:prefix].present?
|
|
21
|
+
@tags = @tags.where('tags.name LIKE ?', "#{params[:prefix]}%")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def show
|
|
27
|
+
@tag = HTM::Models::Tag.find(params[:id])
|
|
28
|
+
@memories = @tag.nodes.active.includes(:tags).order(created_at: :desc)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Import and register all your controllers from the importmap via controllers/**/*_controller
|
|
2
|
+
import { application } from "controllers/application"
|
|
3
|
+
|
|
4
|
+
// Eager load all controllers defined in the import map under controllers/**/*_controller
|
|
5
|
+
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
|
6
|
+
eagerLoadControllersFrom("controllers", application)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<div class="space-y-8">
|
|
2
|
+
<!-- Header -->
|
|
3
|
+
<div>
|
|
4
|
+
<h1 class="text-2xl font-bold text-white">HTM Memory Explorer</h1>
|
|
5
|
+
<p class="mt-1 text-sm text-gray-400">Monitor and explore your Hierarchical Temporal Memory system</p>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<!-- Stats Grid -->
|
|
9
|
+
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
10
|
+
<%= render 'shared/stat_card', title: 'Total Memories', value: @stats[:total_nodes], icon: 'memory', color: 'indigo' %>
|
|
11
|
+
<%= render 'shared/stat_card', title: 'With Embeddings', value: @stats[:nodes_with_embeddings], icon: 'vector', color: 'green' %>
|
|
12
|
+
<%= render 'shared/stat_card', title: 'Deleted', value: @stats[:deleted_nodes], icon: 'trash', color: 'red' %>
|
|
13
|
+
<%= render 'shared/stat_card', title: 'Tags', value: @stats[:total_tags], icon: 'tag', color: 'yellow' %>
|
|
14
|
+
<%= render 'shared/stat_card', title: 'Robots', value: @stats[:total_robots], icon: 'robot', color: 'purple' %>
|
|
15
|
+
<%= render 'shared/stat_card', title: 'File Sources', value: @stats[:total_file_sources], icon: 'file', color: 'blue' %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- Two Column Layout -->
|
|
19
|
+
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
20
|
+
<!-- Recent Memories -->
|
|
21
|
+
<div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
|
|
22
|
+
<div class="flex items-center justify-between mb-4">
|
|
23
|
+
<h2 class="text-lg font-medium text-white">Recent Memories</h2>
|
|
24
|
+
<%= link_to 'View All', memories_path, class: 'text-sm text-indigo-400 hover:text-indigo-300' %>
|
|
25
|
+
</div>
|
|
26
|
+
<% if @recent_memories.any? %>
|
|
27
|
+
<ul class="space-y-3">
|
|
28
|
+
<% @recent_memories.each do |memory| %>
|
|
29
|
+
<li class="rounded-md bg-gray-700/50 p-3">
|
|
30
|
+
<p class="text-sm text-gray-300 line-clamp-2"><%= truncate(memory.content, length: 120) %></p>
|
|
31
|
+
<div class="mt-2 flex items-center gap-2 text-xs text-gray-500">
|
|
32
|
+
<span><%= time_ago_in_words(memory.created_at) %> ago</span>
|
|
33
|
+
<% if memory.embedding.present? %>
|
|
34
|
+
<span class="inline-flex items-center rounded-full bg-green-900/50 px-2 py-0.5 text-xs text-green-400">embedded</span>
|
|
35
|
+
<% end %>
|
|
36
|
+
</div>
|
|
37
|
+
</li>
|
|
38
|
+
<% end %>
|
|
39
|
+
</ul>
|
|
40
|
+
<% else %>
|
|
41
|
+
<p class="text-sm text-gray-500">No memories stored yet. <%= link_to 'Add one', new_memory_path, class: 'text-indigo-400 hover:text-indigo-300' %>.</p>
|
|
42
|
+
<% end %>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Top Tags -->
|
|
46
|
+
<div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
|
|
47
|
+
<div class="flex items-center justify-between mb-4">
|
|
48
|
+
<h2 class="text-lg font-medium text-white">Top Tags</h2>
|
|
49
|
+
<%= link_to 'View All', tags_path, class: 'text-sm text-indigo-400 hover:text-indigo-300' %>
|
|
50
|
+
</div>
|
|
51
|
+
<% if @top_tags.any? %>
|
|
52
|
+
<div class="flex flex-wrap gap-2">
|
|
53
|
+
<% @top_tags.each do |tag| %>
|
|
54
|
+
<%= link_to tag_path(tag), class: 'group' do %>
|
|
55
|
+
<span class="inline-flex items-center gap-1 rounded-full bg-indigo-900/50 border border-indigo-500/30 px-3 py-1 text-sm text-indigo-300 hover:bg-indigo-800/50 hover:border-indigo-500/50 transition-colors">
|
|
56
|
+
<%= tag.name %>
|
|
57
|
+
<span class="text-xs text-indigo-400/70">(<%= tag.node_count %>)</span>
|
|
58
|
+
</span>
|
|
59
|
+
<% end %>
|
|
60
|
+
<% end %>
|
|
61
|
+
</div>
|
|
62
|
+
<% else %>
|
|
63
|
+
<p class="text-sm text-gray-500">No tags created yet.</p>
|
|
64
|
+
<% end %>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- Robots Section -->
|
|
69
|
+
<div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
|
|
70
|
+
<div class="flex items-center justify-between mb-4">
|
|
71
|
+
<h2 class="text-lg font-medium text-white">Active Robots</h2>
|
|
72
|
+
<%= link_to 'View All', robots_path, class: 'text-sm text-indigo-400 hover:text-indigo-300' %>
|
|
73
|
+
</div>
|
|
74
|
+
<% if @robots.any? %>
|
|
75
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
76
|
+
<% @robots.each do |robot| %>
|
|
77
|
+
<%= link_to robot_path(robot), class: 'block rounded-md bg-gray-700/50 p-4 hover:bg-gray-700 transition-colors' do %>
|
|
78
|
+
<div class="flex items-center gap-3">
|
|
79
|
+
<div class="flex-shrink-0">
|
|
80
|
+
<div class="h-10 w-10 rounded-full bg-purple-900/50 flex items-center justify-center">
|
|
81
|
+
<svg class="h-5 w-5 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
82
|
+
<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" />
|
|
83
|
+
</svg>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div>
|
|
87
|
+
<p class="text-sm font-medium text-white"><%= robot.name %></p>
|
|
88
|
+
<p class="text-xs text-gray-500">Created <%= time_ago_in_words(robot.created_at) %> ago</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<% end %>
|
|
92
|
+
<% end %>
|
|
93
|
+
</div>
|
|
94
|
+
<% else %>
|
|
95
|
+
<p class="text-sm text-gray-500">No robots registered yet.</p>
|
|
96
|
+
<% end %>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Quick Actions -->
|
|
100
|
+
<div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
|
|
101
|
+
<h2 class="text-lg font-medium text-white mb-4">Quick Actions</h2>
|
|
102
|
+
<div class="flex flex-wrap gap-3">
|
|
103
|
+
<%= link_to new_memory_path, class: 'inline-flex items-center gap-2 rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 transition-colors' do %>
|
|
104
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
105
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
106
|
+
</svg>
|
|
107
|
+
Add Memory
|
|
108
|
+
<% end %>
|
|
109
|
+
<%= link_to search_path, class: 'inline-flex items-center gap-2 rounded-md bg-gray-700 px-4 py-2 text-sm font-medium text-white hover:bg-gray-600 transition-colors' do %>
|
|
110
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
111
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
112
|
+
</svg>
|
|
113
|
+
Search Memories
|
|
114
|
+
<% end %>
|
|
115
|
+
<%= link_to tags_path(view: 'tree'), class: 'inline-flex items-center gap-2 rounded-md bg-gray-700 px-4 py-2 text-sm font-medium text-white hover:bg-gray-600 transition-colors' do %>
|
|
116
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
117
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7" />
|
|
118
|
+
</svg>
|
|
119
|
+
View Tag Tree
|
|
120
|
+
<% end %>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|