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,488 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fast_mcp'
|
|
4
|
+
|
|
5
|
+
class HTM
|
|
6
|
+
module MCP
|
|
7
|
+
# Session state for the current robot
|
|
8
|
+
# Each MCP client spawns its own server process, so this is naturally isolated
|
|
9
|
+
module Session
|
|
10
|
+
DEFAULT_ROBOT_NAME = "mcp_default"
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
attr_accessor :logger
|
|
14
|
+
|
|
15
|
+
def htm_instance
|
|
16
|
+
@htm_instance ||= HTM.new(robot_name: DEFAULT_ROBOT_NAME)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def set_robot(name)
|
|
20
|
+
@robot_name = name
|
|
21
|
+
@htm_instance = HTM.new(robot_name: name)
|
|
22
|
+
logger&.info "Robot set: #{name} (id=#{@htm_instance.robot_id})"
|
|
23
|
+
@htm_instance
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def robot_name
|
|
27
|
+
@robot_name || DEFAULT_ROBOT_NAME
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def robot_initialized?
|
|
31
|
+
@robot_name != nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Tool: Set the robot identity for this session
|
|
37
|
+
class SetRobotTool < FastMcp::Tool
|
|
38
|
+
description "Set the robot identity for this session. Call this first to establish your robot name."
|
|
39
|
+
|
|
40
|
+
arguments do
|
|
41
|
+
required(:name).filled(:string).description("The robot name (will be created if it doesn't exist)")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def call(name:)
|
|
45
|
+
Session.logger&.info "SetRobotTool called: name=#{name.inspect}"
|
|
46
|
+
|
|
47
|
+
htm = Session.set_robot(name)
|
|
48
|
+
robot = HTM::Models::Robot.find(htm.robot_id)
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
success: true,
|
|
52
|
+
robot_id: htm.robot_id,
|
|
53
|
+
robot_name: htm.robot_name,
|
|
54
|
+
node_count: robot.node_count,
|
|
55
|
+
message: "Robot '#{name}' is now active for this session"
|
|
56
|
+
}.to_json
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Tool: Get current robot info
|
|
61
|
+
class GetRobotTool < FastMcp::Tool
|
|
62
|
+
description "Get information about the current robot for this session"
|
|
63
|
+
|
|
64
|
+
arguments do
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def call
|
|
68
|
+
Session.logger&.info "GetRobotTool called"
|
|
69
|
+
|
|
70
|
+
htm = Session.htm_instance
|
|
71
|
+
robot = HTM::Models::Robot.find(htm.robot_id)
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
success: true,
|
|
75
|
+
robot_id: htm.robot_id,
|
|
76
|
+
robot_name: htm.robot_name,
|
|
77
|
+
initialized: Session.robot_initialized?,
|
|
78
|
+
memory_summary: robot.memory_summary
|
|
79
|
+
}.to_json
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Tool: Get working memory contents for session restore
|
|
84
|
+
class GetWorkingMemoryTool < FastMcp::Tool
|
|
85
|
+
description "Get all working memory contents for the current robot. Use this to restore a previous session."
|
|
86
|
+
|
|
87
|
+
arguments do
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def call
|
|
91
|
+
htm = Session.htm_instance
|
|
92
|
+
robot = HTM::Models::Robot.find(htm.robot_id)
|
|
93
|
+
Session.logger&.info "GetWorkingMemoryTool called for robot=#{htm.robot_name}"
|
|
94
|
+
|
|
95
|
+
# Get all nodes in working memory with their metadata
|
|
96
|
+
# Filter out any robot_nodes where the node has been deleted (node uses default_scope)
|
|
97
|
+
working_memory_nodes = robot.robot_nodes
|
|
98
|
+
.in_working_memory
|
|
99
|
+
.joins(:node) # Inner join excludes deleted nodes
|
|
100
|
+
.includes(node: :tags)
|
|
101
|
+
.order(last_remembered_at: :desc)
|
|
102
|
+
.filter_map do |rn|
|
|
103
|
+
next unless rn.node # Extra safety check
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
id: rn.node.id,
|
|
107
|
+
content: rn.node.content,
|
|
108
|
+
tags: rn.node.tags.map(&:name),
|
|
109
|
+
remember_count: rn.remember_count,
|
|
110
|
+
last_remembered_at: rn.last_remembered_at&.iso8601,
|
|
111
|
+
created_at: rn.node.created_at.iso8601
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
Session.logger&.info "GetWorkingMemoryTool complete: #{working_memory_nodes.length} nodes in working memory"
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
success: true,
|
|
119
|
+
robot_id: htm.robot_id,
|
|
120
|
+
robot_name: htm.robot_name,
|
|
121
|
+
count: working_memory_nodes.length,
|
|
122
|
+
working_memory: working_memory_nodes
|
|
123
|
+
}.to_json
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
Session.logger&.error "GetWorkingMemoryTool error: #{e.message}"
|
|
126
|
+
{ success: false, error: e.message, count: 0, working_memory: [] }.to_json
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Tool: Remember information
|
|
131
|
+
class RememberTool < FastMcp::Tool
|
|
132
|
+
description "Store information in HTM long-term memory with optional tags"
|
|
133
|
+
|
|
134
|
+
arguments do
|
|
135
|
+
required(:content).filled(:string).description("The content to remember")
|
|
136
|
+
optional(:tags).array(:string).description("Optional tags for categorization (e.g., ['database:postgresql', 'config'])")
|
|
137
|
+
optional(:metadata).hash.description("Optional metadata key-value pairs")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def call(content:, tags: [], metadata: {})
|
|
141
|
+
Session.logger&.info "RememberTool called: content=#{content[0..50].inspect}..."
|
|
142
|
+
|
|
143
|
+
htm = Session.htm_instance
|
|
144
|
+
node_id = htm.remember(content, tags: tags, metadata: metadata)
|
|
145
|
+
node = HTM::Models::Node.includes(:tags).find(node_id)
|
|
146
|
+
|
|
147
|
+
Session.logger&.info "Memory stored: node_id=#{node_id}, robot=#{htm.robot_name}, tags=#{node.tags.map(&:name)}"
|
|
148
|
+
|
|
149
|
+
{
|
|
150
|
+
success: true,
|
|
151
|
+
node_id: node_id,
|
|
152
|
+
robot_id: htm.robot_id,
|
|
153
|
+
robot_name: htm.robot_name,
|
|
154
|
+
content: node.content,
|
|
155
|
+
tags: node.tags.map(&:name),
|
|
156
|
+
created_at: node.created_at.iso8601
|
|
157
|
+
}.to_json
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Tool: Recall memories
|
|
162
|
+
class RecallTool < FastMcp::Tool
|
|
163
|
+
description "Search and retrieve memories from HTM using semantic, full-text, or hybrid search"
|
|
164
|
+
|
|
165
|
+
arguments do
|
|
166
|
+
required(:query).filled(:string).description("Search query - can be natural language or keywords")
|
|
167
|
+
optional(:limit).filled(:integer).description("Maximum number of results (default: 10)")
|
|
168
|
+
optional(:strategy).filled(:string).description("Search strategy: 'vector', 'fulltext', or 'hybrid' (default: 'hybrid')")
|
|
169
|
+
optional(:timeframe).filled(:string).description("Filter by time: 'today', 'this week', 'this month', or ISO8601 date range")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def call(query:, limit: 10, strategy: 'hybrid', timeframe: nil)
|
|
173
|
+
htm = Session.htm_instance
|
|
174
|
+
Session.logger&.info "RecallTool called: query=#{query.inspect}, strategy=#{strategy}, limit=#{limit}, robot=#{htm.robot_name}"
|
|
175
|
+
|
|
176
|
+
recall_opts = {
|
|
177
|
+
limit: limit,
|
|
178
|
+
strategy: strategy.to_sym,
|
|
179
|
+
raw: true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Parse timeframe if provided
|
|
183
|
+
if timeframe
|
|
184
|
+
recall_opts[:timeframe] = parse_timeframe(timeframe)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
memories = htm.recall(query, **recall_opts)
|
|
188
|
+
|
|
189
|
+
results = memories.map do |memory|
|
|
190
|
+
node = HTM::Models::Node.includes(:tags).find(memory['id'])
|
|
191
|
+
{
|
|
192
|
+
id: memory['id'],
|
|
193
|
+
content: memory['content'],
|
|
194
|
+
tags: node.tags.map(&:name),
|
|
195
|
+
created_at: memory['created_at'],
|
|
196
|
+
score: memory['combined_score'] || memory['similarity']
|
|
197
|
+
}
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
Session.logger&.info "Recall complete: found #{results.length} memories"
|
|
201
|
+
|
|
202
|
+
{
|
|
203
|
+
success: true,
|
|
204
|
+
query: query,
|
|
205
|
+
strategy: strategy,
|
|
206
|
+
robot_name: htm.robot_name,
|
|
207
|
+
count: results.length,
|
|
208
|
+
results: results
|
|
209
|
+
}.to_json
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
private
|
|
213
|
+
|
|
214
|
+
def parse_timeframe(timeframe)
|
|
215
|
+
case timeframe.downcase
|
|
216
|
+
when 'today'
|
|
217
|
+
Time.now.beginning_of_day..Time.now
|
|
218
|
+
when 'this week'
|
|
219
|
+
1.week.ago..Time.now
|
|
220
|
+
when 'this month'
|
|
221
|
+
1.month.ago..Time.now
|
|
222
|
+
else
|
|
223
|
+
# Try to parse as ISO8601 range (start..end)
|
|
224
|
+
if timeframe.include?('..')
|
|
225
|
+
parts = timeframe.split('..')
|
|
226
|
+
Time.parse(parts[0])..Time.parse(parts[1])
|
|
227
|
+
else
|
|
228
|
+
# Single date - from that date to now
|
|
229
|
+
Time.parse(timeframe)..Time.now
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
rescue ArgumentError
|
|
233
|
+
nil # Invalid timeframe, skip filtering
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Tool: Forget a memory
|
|
238
|
+
class ForgetTool < FastMcp::Tool
|
|
239
|
+
description "Soft-delete a memory from HTM (can be restored later)"
|
|
240
|
+
|
|
241
|
+
arguments do
|
|
242
|
+
required(:node_id).filled(:integer).description("The ID of the node to forget")
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def call(node_id:)
|
|
246
|
+
htm = Session.htm_instance
|
|
247
|
+
Session.logger&.info "ForgetTool called: node_id=#{node_id}, robot=#{htm.robot_name}"
|
|
248
|
+
|
|
249
|
+
htm.forget(node_id)
|
|
250
|
+
|
|
251
|
+
Session.logger&.info "Memory soft-deleted: node_id=#{node_id}"
|
|
252
|
+
|
|
253
|
+
{
|
|
254
|
+
success: true,
|
|
255
|
+
node_id: node_id,
|
|
256
|
+
robot_name: htm.robot_name,
|
|
257
|
+
message: "Memory soft-deleted. Use restore to recover."
|
|
258
|
+
}.to_json
|
|
259
|
+
rescue HTM::NotFoundError, ActiveRecord::RecordNotFound
|
|
260
|
+
Session.logger&.warn "ForgetTool failed: node #{node_id} not found"
|
|
261
|
+
{
|
|
262
|
+
success: false,
|
|
263
|
+
error: "Node #{node_id} not found"
|
|
264
|
+
}.to_json
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Tool: Restore a forgotten memory
|
|
269
|
+
class RestoreTool < FastMcp::Tool
|
|
270
|
+
description "Restore a soft-deleted memory"
|
|
271
|
+
|
|
272
|
+
arguments do
|
|
273
|
+
required(:node_id).filled(:integer).description("The ID of the node to restore")
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def call(node_id:)
|
|
277
|
+
htm = Session.htm_instance
|
|
278
|
+
Session.logger&.info "RestoreTool called: node_id=#{node_id}, robot=#{htm.robot_name}"
|
|
279
|
+
|
|
280
|
+
htm.restore(node_id)
|
|
281
|
+
|
|
282
|
+
Session.logger&.info "Memory restored: node_id=#{node_id}"
|
|
283
|
+
|
|
284
|
+
{
|
|
285
|
+
success: true,
|
|
286
|
+
node_id: node_id,
|
|
287
|
+
robot_name: htm.robot_name,
|
|
288
|
+
message: "Memory restored successfully"
|
|
289
|
+
}.to_json
|
|
290
|
+
rescue HTM::NotFoundError, ActiveRecord::RecordNotFound
|
|
291
|
+
Session.logger&.warn "RestoreTool failed: node #{node_id} not found"
|
|
292
|
+
{
|
|
293
|
+
success: false,
|
|
294
|
+
error: "Node #{node_id} not found"
|
|
295
|
+
}.to_json
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Tool: List tags
|
|
300
|
+
class ListTagsTool < FastMcp::Tool
|
|
301
|
+
description "List all tags in HTM, optionally filtered by prefix"
|
|
302
|
+
|
|
303
|
+
arguments do
|
|
304
|
+
optional(:prefix).filled(:string).description("Filter tags by prefix (e.g., 'database' returns 'database:postgresql', etc.)")
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def call(prefix: nil)
|
|
308
|
+
Session.logger&.info "ListTagsTool called: prefix=#{prefix.inspect}"
|
|
309
|
+
|
|
310
|
+
tags_query = HTM::Models::Tag.order(:name)
|
|
311
|
+
tags_query = tags_query.where("name LIKE ?", "#{prefix}%") if prefix
|
|
312
|
+
|
|
313
|
+
tags = tags_query.map do |tag|
|
|
314
|
+
{
|
|
315
|
+
name: tag.name,
|
|
316
|
+
node_count: tag.nodes.count
|
|
317
|
+
}
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
Session.logger&.info "ListTagsTool complete: found #{tags.length} tags"
|
|
321
|
+
|
|
322
|
+
{
|
|
323
|
+
success: true,
|
|
324
|
+
prefix: prefix,
|
|
325
|
+
count: tags.length,
|
|
326
|
+
tags: tags
|
|
327
|
+
}.to_json
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Tool: Search tags with fuzzy matching
|
|
332
|
+
class SearchTagsTool < FastMcp::Tool
|
|
333
|
+
description "Search for tags using fuzzy matching (typo-tolerant). Use this when you're unsure of exact tag names."
|
|
334
|
+
|
|
335
|
+
arguments do
|
|
336
|
+
required(:query).filled(:string).description("Search query - can contain typos (e.g., 'postgrsql' finds 'database:postgresql')")
|
|
337
|
+
optional(:limit).filled(:integer).description("Maximum number of results (default: 20)")
|
|
338
|
+
optional(:min_similarity).filled(:float).description("Minimum similarity threshold 0.0-1.0 (default: 0.3, lower = more fuzzy)")
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def call(query:, limit: 20, min_similarity: 0.3)
|
|
342
|
+
Session.logger&.info "SearchTagsTool called: query=#{query.inspect}, limit=#{limit}, min_similarity=#{min_similarity}"
|
|
343
|
+
|
|
344
|
+
htm = Session.htm_instance
|
|
345
|
+
ltm = htm.instance_variable_get(:@long_term_memory)
|
|
346
|
+
|
|
347
|
+
results = ltm.search_tags(query, limit: limit, min_similarity: min_similarity)
|
|
348
|
+
|
|
349
|
+
# Enrich with node counts
|
|
350
|
+
tags = results.map do |result|
|
|
351
|
+
tag = HTM::Models::Tag.find_by(name: result[:name])
|
|
352
|
+
{
|
|
353
|
+
name: result[:name],
|
|
354
|
+
similarity: result[:similarity].round(3),
|
|
355
|
+
node_count: tag&.nodes&.count || 0
|
|
356
|
+
}
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
Session.logger&.info "SearchTagsTool complete: found #{tags.length} tags"
|
|
360
|
+
|
|
361
|
+
{
|
|
362
|
+
success: true,
|
|
363
|
+
query: query,
|
|
364
|
+
min_similarity: min_similarity,
|
|
365
|
+
count: tags.length,
|
|
366
|
+
tags: tags
|
|
367
|
+
}.to_json
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Tool: Find nodes by topic with fuzzy option
|
|
372
|
+
class FindByTopicTool < FastMcp::Tool
|
|
373
|
+
description "Find memory nodes by topic/tag with optional fuzzy matching for typo tolerance"
|
|
374
|
+
|
|
375
|
+
arguments do
|
|
376
|
+
required(:topic).filled(:string).description("Topic or tag to search for (e.g., 'database:postgresql' or 'postgrsql' with fuzzy)")
|
|
377
|
+
optional(:fuzzy).filled(:bool).description("Enable fuzzy matching for typo tolerance (default: false)")
|
|
378
|
+
optional(:exact).filled(:bool).description("Require exact tag match (default: false, uses prefix matching)")
|
|
379
|
+
optional(:limit).filled(:integer).description("Maximum number of results (default: 20)")
|
|
380
|
+
optional(:min_similarity).filled(:float).description("Minimum similarity for fuzzy mode (default: 0.3)")
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def call(topic:, fuzzy: false, exact: false, limit: 20, min_similarity: 0.3)
|
|
384
|
+
Session.logger&.info "FindByTopicTool called: topic=#{topic.inspect}, fuzzy=#{fuzzy}, exact=#{exact}"
|
|
385
|
+
|
|
386
|
+
htm = Session.htm_instance
|
|
387
|
+
ltm = htm.instance_variable_get(:@long_term_memory)
|
|
388
|
+
|
|
389
|
+
nodes = ltm.nodes_by_topic(
|
|
390
|
+
topic,
|
|
391
|
+
fuzzy: fuzzy,
|
|
392
|
+
exact: exact,
|
|
393
|
+
min_similarity: min_similarity,
|
|
394
|
+
limit: limit
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Enrich with tags
|
|
398
|
+
results = nodes.map do |node_attrs|
|
|
399
|
+
node = HTM::Models::Node.includes(:tags).find_by(id: node_attrs['id'])
|
|
400
|
+
next unless node
|
|
401
|
+
|
|
402
|
+
{
|
|
403
|
+
id: node.id,
|
|
404
|
+
content: node.content[0..200],
|
|
405
|
+
tags: node.tags.map(&:name),
|
|
406
|
+
created_at: node.created_at.iso8601
|
|
407
|
+
}
|
|
408
|
+
end.compact
|
|
409
|
+
|
|
410
|
+
Session.logger&.info "FindByTopicTool complete: found #{results.length} nodes"
|
|
411
|
+
|
|
412
|
+
{
|
|
413
|
+
success: true,
|
|
414
|
+
topic: topic,
|
|
415
|
+
fuzzy: fuzzy,
|
|
416
|
+
exact: exact,
|
|
417
|
+
count: results.length,
|
|
418
|
+
results: results
|
|
419
|
+
}.to_json
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Tool: Get memory statistics
|
|
424
|
+
class StatsTool < FastMcp::Tool
|
|
425
|
+
description "Get statistics about HTM memory usage"
|
|
426
|
+
|
|
427
|
+
arguments do
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def call
|
|
431
|
+
htm = Session.htm_instance
|
|
432
|
+
robot = HTM::Models::Robot.find(htm.robot_id)
|
|
433
|
+
Session.logger&.info "StatsTool called for robot=#{htm.robot_name}"
|
|
434
|
+
|
|
435
|
+
# Note: Node uses default_scope to exclude deleted, so .count returns active nodes
|
|
436
|
+
total_nodes = HTM::Models::Node.count
|
|
437
|
+
deleted_nodes = HTM::Models::Node.deleted.count
|
|
438
|
+
nodes_with_embeddings = HTM::Models::Node.with_embeddings.count
|
|
439
|
+
nodes_with_tags = HTM::Models::Node.joins(:tags).distinct.count
|
|
440
|
+
total_tags = HTM::Models::Tag.count
|
|
441
|
+
total_robots = HTM::Models::Robot.count
|
|
442
|
+
|
|
443
|
+
Session.logger&.info "StatsTool complete: #{total_nodes} active nodes, #{total_tags} tags"
|
|
444
|
+
|
|
445
|
+
{
|
|
446
|
+
success: true,
|
|
447
|
+
current_robot: {
|
|
448
|
+
name: htm.robot_name,
|
|
449
|
+
id: htm.robot_id,
|
|
450
|
+
memory_summary: robot.memory_summary
|
|
451
|
+
},
|
|
452
|
+
statistics: {
|
|
453
|
+
nodes: {
|
|
454
|
+
active: total_nodes,
|
|
455
|
+
deleted: deleted_nodes,
|
|
456
|
+
with_embeddings: nodes_with_embeddings,
|
|
457
|
+
with_tags: nodes_with_tags
|
|
458
|
+
},
|
|
459
|
+
tags: {
|
|
460
|
+
total: total_tags
|
|
461
|
+
},
|
|
462
|
+
robots: {
|
|
463
|
+
total: total_robots
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}.to_json
|
|
467
|
+
rescue StandardError => e
|
|
468
|
+
Session.logger&.error "StatsTool error: #{e.message}"
|
|
469
|
+
{ success: false, error: e.message }.to_json
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# All individual tools for registration
|
|
474
|
+
TOOLS = [
|
|
475
|
+
SetRobotTool,
|
|
476
|
+
GetRobotTool,
|
|
477
|
+
GetWorkingMemoryTool,
|
|
478
|
+
RememberTool,
|
|
479
|
+
RecallTool,
|
|
480
|
+
ForgetTool,
|
|
481
|
+
RestoreTool,
|
|
482
|
+
ListTagsTool,
|
|
483
|
+
SearchTagsTool,
|
|
484
|
+
FindByTopicTool,
|
|
485
|
+
StatsTool
|
|
486
|
+
].freeze
|
|
487
|
+
end
|
|
488
|
+
end
|
|
@@ -41,12 +41,14 @@ class HTM
|
|
|
41
41
|
# - Floating-point rounding errors
|
|
42
42
|
# - Minor timestamp discrepancies across systems
|
|
43
43
|
#
|
|
44
|
-
# @param current_mtime [Time] Current file modification time
|
|
45
|
-
# @return [Boolean] true if file modification time differs by more than DELTA_TIME
|
|
44
|
+
# @param current_mtime [Time, nil] Current file modification time (defaults to reading from filesystem)
|
|
45
|
+
# @return [Boolean] true if file modification time differs by more than DELTA_TIME, or file doesn't exist
|
|
46
46
|
#
|
|
47
|
-
def needs_sync?(current_mtime)
|
|
47
|
+
def needs_sync?(current_mtime = nil)
|
|
48
48
|
return true if mtime.nil?
|
|
49
|
+
return true unless File.exist?(file_path)
|
|
49
50
|
|
|
51
|
+
current_mtime ||= File.mtime(file_path)
|
|
50
52
|
(current_mtime.to_i - mtime.to_i).abs > DELTA_TIME
|
|
51
53
|
end
|
|
52
54
|
|
data/lib/htm/railtie.rb
CHANGED
data/lib/htm/tasks.rb
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
#
|
|
9
9
|
# This will make the following tasks available:
|
|
10
10
|
#
|
|
11
|
-
# Database tasks (all respect RAILS_ENV, default: development):
|
|
11
|
+
# Database tasks (all respect HTM_ENV/RAILS_ENV, default: development):
|
|
12
12
|
# rake htm:db:create # Create database if it doesn't exist
|
|
13
13
|
# rake htm:db:setup # Set up HTM database schema and run migrations
|
|
14
14
|
# rake htm:db:migrate # Run pending database migrations
|
|
@@ -20,10 +20,13 @@
|
|
|
20
20
|
# rake htm:db:drop # Drop all HTM tables (destructive!)
|
|
21
21
|
# rake htm:db:reset # Drop and recreate database (destructive!)
|
|
22
22
|
#
|
|
23
|
+
# Environment detection priority: HTM_ENV > RAILS_ENV > RACK_ENV > 'development'
|
|
24
|
+
#
|
|
23
25
|
# Examples:
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
26
|
+
# HTM_ENV=test rake htm:db:create # Create htm_test database
|
|
27
|
+
# HTM_ENV=test rake htm:db:setup # Setup test database with migrations
|
|
28
|
+
# HTM_ENV=test rake htm:db:drop # Drop test database
|
|
29
|
+
# RAILS_ENV=test rake htm:db:create # Also works (for Rails apps)
|
|
27
30
|
#
|
|
28
31
|
# Async job tasks:
|
|
29
32
|
# rake htm:jobs:stats # Show async job statistics
|
data/lib/htm/version.rb
CHANGED
data/lib/tasks/htm.rake
CHANGED
|
@@ -68,14 +68,13 @@ namespace :htm do
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
desc "Verify database connection (respects RAILS_ENV)"
|
|
71
|
+
desc "Verify database connection (respects HTM_ENV/RAILS_ENV)"
|
|
72
72
|
task :verify do
|
|
73
73
|
require 'htm'
|
|
74
74
|
|
|
75
|
-
env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
|
76
75
|
config = HTM::ActiveRecordConfig.load_database_config
|
|
77
76
|
|
|
78
|
-
puts "Verifying HTM database connection (#{env})..."
|
|
77
|
+
puts "Verifying HTM database connection (#{HTM.env})..."
|
|
79
78
|
puts " Host: #{config[:host]}"
|
|
80
79
|
puts " Port: #{config[:port]}"
|
|
81
80
|
puts " Database: #{config[:database]}"
|
|
@@ -107,14 +106,13 @@ namespace :htm do
|
|
|
107
106
|
end
|
|
108
107
|
end
|
|
109
108
|
|
|
110
|
-
desc "Open PostgreSQL console (respects RAILS_ENV)"
|
|
109
|
+
desc "Open PostgreSQL console (respects HTM_ENV/RAILS_ENV)"
|
|
111
110
|
task :console do
|
|
112
111
|
require 'htm'
|
|
113
112
|
|
|
114
|
-
env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
|
115
113
|
config = HTM::ActiveRecordConfig.load_database_config
|
|
116
114
|
|
|
117
|
-
puts "Connecting to #{config[:database]} (#{env})..."
|
|
115
|
+
puts "Connecting to #{config[:database]} (#{HTM.env})..."
|
|
118
116
|
exec "psql", "-h", config[:host],
|
|
119
117
|
"-p", config[:port].to_s,
|
|
120
118
|
"-U", config[:username],
|
|
@@ -416,15 +414,14 @@ namespace :htm do
|
|
|
416
414
|
end
|
|
417
415
|
end
|
|
418
416
|
|
|
419
|
-
desc "Create database if it doesn't exist (respects RAILS_ENV)"
|
|
417
|
+
desc "Create database if it doesn't exist (respects HTM_ENV/RAILS_ENV)"
|
|
420
418
|
task :create do
|
|
421
419
|
require 'htm'
|
|
422
420
|
|
|
423
|
-
env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
|
424
421
|
config = HTM::ActiveRecordConfig.load_database_config
|
|
425
422
|
db_name = config[:database]
|
|
426
423
|
|
|
427
|
-
puts "Creating database: #{db_name} (#{env})"
|
|
424
|
+
puts "Creating database: #{db_name} (#{HTM.env})"
|
|
428
425
|
|
|
429
426
|
admin_config = config.dup
|
|
430
427
|
admin_config[:database] = 'postgres'
|