htm 0.0.10 → 0.0.14
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/.dictate.toml +46 -0
- data/.envrc +2 -0
- data/CHANGELOG.md +86 -3
- data/README.md +86 -7
- data/Rakefile +14 -2
- data/bin/htm_mcp.rb +621 -0
- data/config/database.yml +20 -13
- data/db/migrate/00010_add_soft_delete_to_associations.rb +29 -0
- data/db/migrate/00011_add_performance_indexes.rb +21 -0
- data/db/migrate/00012_add_tags_trigram_index.rb +18 -0
- data/db/migrate/00013_enable_lz4_compression.rb +43 -0
- data/db/schema.sql +49 -92
- data/docs/api/index.md +1 -1
- data/docs/api/yard/HTM.md +2 -4
- data/docs/architecture/index.md +1 -1
- data/docs/development/index.md +1 -1
- data/docs/getting-started/index.md +1 -1
- data/docs/guides/index.md +1 -1
- data/docs/images/telemetry-architecture.svg +153 -0
- data/docs/telemetry.md +391 -0
- data/examples/README.md +171 -1
- data/examples/cli_app/README.md +1 -1
- data/examples/cli_app/htm_cli.rb +1 -1
- data/examples/mcp_client.rb +529 -0
- data/examples/sinatra_app/app.rb +1 -1
- data/examples/telemetry/README.md +147 -0
- data/examples/telemetry/SETUP_README.md +169 -0
- data/examples/telemetry/demo.rb +498 -0
- data/examples/telemetry/grafana/dashboards/htm-metrics.json +457 -0
- data/lib/htm/configuration.rb +261 -70
- data/lib/htm/database.rb +46 -22
- data/lib/htm/embedding_service.rb +24 -14
- data/lib/htm/errors.rb +15 -1
- data/lib/htm/jobs/generate_embedding_job.rb +19 -0
- data/lib/htm/jobs/generate_propositions_job.rb +103 -0
- data/lib/htm/jobs/generate_tags_job.rb +24 -0
- data/lib/htm/loaders/markdown_chunker.rb +79 -0
- data/lib/htm/loaders/markdown_loader.rb +41 -15
- data/lib/htm/long_term_memory/fulltext_search.rb +138 -0
- data/lib/htm/long_term_memory/hybrid_search.rb +324 -0
- data/lib/htm/long_term_memory/node_operations.rb +209 -0
- data/lib/htm/long_term_memory/relevance_scorer.rb +355 -0
- data/lib/htm/long_term_memory/robot_operations.rb +34 -0
- data/lib/htm/long_term_memory/tag_operations.rb +428 -0
- data/lib/htm/long_term_memory/vector_search.rb +109 -0
- data/lib/htm/long_term_memory.rb +51 -1153
- data/lib/htm/models/node.rb +35 -2
- data/lib/htm/models/node_tag.rb +31 -0
- data/lib/htm/models/robot_node.rb +31 -0
- data/lib/htm/models/tag.rb +44 -0
- data/lib/htm/proposition_service.rb +169 -0
- data/lib/htm/query_cache.rb +214 -0
- data/lib/htm/sql_builder.rb +178 -0
- data/lib/htm/tag_service.rb +16 -6
- data/lib/htm/tasks.rb +8 -2
- data/lib/htm/telemetry.rb +224 -0
- data/lib/htm/version.rb +1 -1
- data/lib/htm.rb +64 -3
- data/lib/tasks/doc.rake +1 -1
- data/lib/tasks/htm.rake +259 -13
- data/mkdocs.yml +96 -96
- metadata +75 -18
- data/.aigcm_msg +0 -1
- data/.claude/settings.local.json +0 -92
- data/CLAUDE.md +0 -603
- data/examples/cli_app/temp.log +0 -93
- data/lib/htm/loaders/paragraph_chunker.rb +0 -112
- data/notes/ARCHITECTURE_REVIEW.md +0 -1167
- data/notes/IMPLEMENTATION_SUMMARY.md +0 -606
- data/notes/MULTI_FRAMEWORK_IMPLEMENTATION.md +0 -451
- data/notes/next_steps.md +0 -100
- data/notes/plan.md +0 -627
- data/notes/tag_ontology_enhancement_ideas.md +0 -222
- data/notes/timescaledb_removal_summary.md +0 -200
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# MCP Client Example for HTM
|
|
5
|
+
#
|
|
6
|
+
# This example demonstrates using ruby_llm-mcp to connect to the
|
|
7
|
+
# HTM MCP server and interact with it through a chat interface
|
|
8
|
+
# using a local Ollama model.
|
|
9
|
+
#
|
|
10
|
+
# Prerequisites:
|
|
11
|
+
# 1. Install gems: gem install ruby_llm-mcp
|
|
12
|
+
# 2. Have Ollama running with gpt-oss model: ollama pull gpt-oss
|
|
13
|
+
# 3. Set HTM_DBURL environment variable
|
|
14
|
+
# 4. The htm_mcp.rb must be available (this client will launch it)
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# ruby examples/mcp_client.rb
|
|
18
|
+
#
|
|
19
|
+
# The client connects to the HTM MCP server via STDIO transport
|
|
20
|
+
# and provides an interactive chat loop where you can:
|
|
21
|
+
# - Ask the LLM to remember information
|
|
22
|
+
# - Query memories using natural language
|
|
23
|
+
# - List tags and statistics
|
|
24
|
+
# - All through conversational AI with tool calling
|
|
25
|
+
|
|
26
|
+
require 'ruby_llm'
|
|
27
|
+
require 'ruby_llm/mcp'
|
|
28
|
+
|
|
29
|
+
# Configuration
|
|
30
|
+
OLLAMA_MODEL = ENV.fetch('OLLAMA_MODEL', 'gpt-oss:latest')
|
|
31
|
+
OLLAMA_URL = ENV.fetch('OLLAMA_URL', 'http://localhost:11434')
|
|
32
|
+
MCP_SERVER_PATH = File.expand_path('../bin/htm_mcp.rb', __dir__)
|
|
33
|
+
ENV_ROBOT_NAME = ENV['HTM_ROBOT_NAME'] # nil if not set, allows prompting
|
|
34
|
+
|
|
35
|
+
class HTMMcpClient
|
|
36
|
+
attr_reader :robot_name
|
|
37
|
+
|
|
38
|
+
def initialize
|
|
39
|
+
validate_environment
|
|
40
|
+
setup_ruby_llm
|
|
41
|
+
setup_mcp_client
|
|
42
|
+
prompt_for_robot_name
|
|
43
|
+
set_robot_identity
|
|
44
|
+
check_working_memory
|
|
45
|
+
setup_chat
|
|
46
|
+
restore_session_if_requested
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run
|
|
51
|
+
print_banner
|
|
52
|
+
chat_loop
|
|
53
|
+
ensure
|
|
54
|
+
cleanup
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def validate_environment
|
|
60
|
+
unless ENV['HTM_DBURL']
|
|
61
|
+
warn 'Error: HTM_DBURL not set.'
|
|
62
|
+
warn ' export HTM_DBURL="postgresql://postgres@localhost:5432/htm_development"'
|
|
63
|
+
exit 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
unless File.exist?(MCP_SERVER_PATH)
|
|
67
|
+
warn "Error: MCP server not found at #{MCP_SERVER_PATH}"
|
|
68
|
+
warn 'Please ensure mcp_server.rb exists in the examples directory.'
|
|
69
|
+
exit 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check Ollama is running
|
|
73
|
+
require 'net/http'
|
|
74
|
+
begin
|
|
75
|
+
uri = URI(OLLAMA_URL)
|
|
76
|
+
Net::HTTP.get_response(uri)
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
warn "Error: Cannot connect to Ollama at #{OLLAMA_URL}"
|
|
79
|
+
warn " #{e.message}"
|
|
80
|
+
warn 'Please ensure Ollama is running: ollama serve'
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def setup_ruby_llm
|
|
87
|
+
# Configure RubyLLM for Ollama
|
|
88
|
+
RubyLLM.configure do |config|
|
|
89
|
+
ollama_api_base = OLLAMA_URL.end_with?('/v1') ? OLLAMA_URL : "#{OLLAMA_URL}/v1"
|
|
90
|
+
config.ollama_api_base = ollama_api_base
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def setup_mcp_client
|
|
96
|
+
puts 'Connecting to HTM MCP server...'
|
|
97
|
+
|
|
98
|
+
@mcp_client = RubyLLM::MCP.client(
|
|
99
|
+
name: 'htm-memory',
|
|
100
|
+
transport_type: :stdio,
|
|
101
|
+
request_timeout: 60_000, # 60 seconds (in ms) for Ollama embedding generation
|
|
102
|
+
config: {
|
|
103
|
+
command: RbConfig.ruby,
|
|
104
|
+
args: [MCP_SERVER_PATH],
|
|
105
|
+
env: {
|
|
106
|
+
'HTM_DBURL' => ENV['HTM_DBURL'],
|
|
107
|
+
'OLLAMA_URL' => OLLAMA_URL
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Wait for server to be ready
|
|
113
|
+
sleep 0.5
|
|
114
|
+
|
|
115
|
+
if @mcp_client.alive?
|
|
116
|
+
puts '[✓] Connected to HTM MCP server'
|
|
117
|
+
else
|
|
118
|
+
warn '[✗] Failed to connect to HTM MCP server'
|
|
119
|
+
exit 1
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# List available tools
|
|
123
|
+
@tools = @mcp_client.tools
|
|
124
|
+
puts "[✓] Found #{@tools.length} tools:"
|
|
125
|
+
@tools.each do |tool|
|
|
126
|
+
puts " - #{tool.name}: #{tool.description[0..60]}..."
|
|
127
|
+
end
|
|
128
|
+
puts
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def prompt_for_robot_name
|
|
133
|
+
if ENV_ROBOT_NAME
|
|
134
|
+
@robot_name = ENV_ROBOT_NAME
|
|
135
|
+
puts "[✓] Using robot name from HTM_ROBOT_NAME: #{@robot_name}"
|
|
136
|
+
return
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
puts
|
|
140
|
+
print 'Enter your robot name (or press Enter for "Assistant"): '
|
|
141
|
+
input = gets&.chomp
|
|
142
|
+
|
|
143
|
+
@robot_name = if input.nil? || input.empty?
|
|
144
|
+
'Assistant'
|
|
145
|
+
else
|
|
146
|
+
input
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
puts "[✓] Robot name: #{@robot_name}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def set_robot_identity
|
|
154
|
+
puts 'Setting robot identity on MCP server...'
|
|
155
|
+
|
|
156
|
+
# Find the SetRobotTool
|
|
157
|
+
set_robot_tool = @tools.find { |t| t.name == 'SetRobotTool' }
|
|
158
|
+
|
|
159
|
+
unless set_robot_tool
|
|
160
|
+
warn '[⚠] SetRobotTool not found - using default robot identity'
|
|
161
|
+
return
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Call SetRobotTool directly via MCP
|
|
165
|
+
result = set_robot_tool.call(name: @robot_name)
|
|
166
|
+
|
|
167
|
+
# Parse the result
|
|
168
|
+
begin
|
|
169
|
+
response = JSON.parse(result.is_a?(String) ? result : result.to_s)
|
|
170
|
+
if response['success']
|
|
171
|
+
puts "[✓] Robot identity set: #{response['robot_name']} (id=#{response['robot_id']}, nodes=#{response['node_count']})"
|
|
172
|
+
else
|
|
173
|
+
warn "[⚠] Failed to set robot identity: #{response['error']}"
|
|
174
|
+
end
|
|
175
|
+
rescue JSON::ParserError => e
|
|
176
|
+
warn "[⚠] Could not parse SetRobotTool response: #{e.message}"
|
|
177
|
+
end
|
|
178
|
+
puts
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def check_working_memory
|
|
183
|
+
# Check if there's working memory to restore
|
|
184
|
+
get_wm_tool = @tools.find { |t| t.name == 'GetWorkingMemoryTool' }
|
|
185
|
+
@working_memory_to_restore = nil
|
|
186
|
+
return unless get_wm_tool
|
|
187
|
+
|
|
188
|
+
result = get_wm_tool.call({})
|
|
189
|
+
|
|
190
|
+
# Extract JSON from MCP Content object
|
|
191
|
+
json_str = result.respond_to?(:text) ? result.text : result.to_s
|
|
192
|
+
response = JSON.parse(json_str)
|
|
193
|
+
|
|
194
|
+
# Debug: show what we got
|
|
195
|
+
puts "[Debug] Working memory check: success=#{response['success']}, count=#{response['count']}"
|
|
196
|
+
|
|
197
|
+
return unless response['success'] && response['count'].to_i > 0
|
|
198
|
+
|
|
199
|
+
puts "Found #{response['count']} memories in working memory from previous session."
|
|
200
|
+
print 'Restore previous session? (y/N): '
|
|
201
|
+
input = gets&.chomp&.downcase
|
|
202
|
+
|
|
203
|
+
if %w[y yes].include?(input)
|
|
204
|
+
@working_memory_to_restore = response['working_memory']
|
|
205
|
+
puts "[✓] Will restore #{@working_memory_to_restore.length} memories after chat setup"
|
|
206
|
+
else
|
|
207
|
+
puts '[✓] Starting fresh session'
|
|
208
|
+
end
|
|
209
|
+
puts
|
|
210
|
+
rescue JSON::ParserError => e
|
|
211
|
+
warn "[⚠] Could not parse working memory response: #{e.message}"
|
|
212
|
+
warn "[⚠] Raw: #{json_str.inspect[0..200]}" if defined?(json_str)
|
|
213
|
+
rescue StandardError => e
|
|
214
|
+
warn "[⚠] Error checking working memory: #{e.class} - #{e.message}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def restore_session_if_requested
|
|
219
|
+
return unless @working_memory_to_restore
|
|
220
|
+
|
|
221
|
+
puts "Restoring #{@working_memory_to_restore.length} memories to chat context..."
|
|
222
|
+
|
|
223
|
+
# Build a system context from the working memory
|
|
224
|
+
context_parts = @working_memory_to_restore.map do |mem|
|
|
225
|
+
tags_str = mem['tags'].empty? ? '' : " [#{mem['tags'].join(', ')}]"
|
|
226
|
+
"- #{mem['content']}#{tags_str}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
context_message = <<~CONTEXT
|
|
230
|
+
The following information was remembered from your previous session with this user:
|
|
231
|
+
|
|
232
|
+
#{context_parts.join("\n")}
|
|
233
|
+
|
|
234
|
+
Use this context to inform your responses, but don't explicitly mention that you're restoring a session unless asked.
|
|
235
|
+
CONTEXT
|
|
236
|
+
|
|
237
|
+
# Add the context as a system message to prime the chat
|
|
238
|
+
@chat.add_message(role: :user, content: "Previous session context: #{context_message}")
|
|
239
|
+
@chat.add_message(role: :assistant,
|
|
240
|
+
content: "I've restored context from our previous session. How can I help you today?")
|
|
241
|
+
|
|
242
|
+
puts "[✓] Restored #{@working_memory_to_restore.length} memories to chat context"
|
|
243
|
+
puts
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def setup_chat
|
|
248
|
+
puts "Initializing chat with #{OLLAMA_MODEL}..."
|
|
249
|
+
|
|
250
|
+
@chat = RubyLLM.chat(
|
|
251
|
+
model: OLLAMA_MODEL,
|
|
252
|
+
provider: :ollama,
|
|
253
|
+
assume_model_exists: true
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Attach MCP tools to the chat
|
|
257
|
+
@chat.with_tools(*@tools)
|
|
258
|
+
|
|
259
|
+
# Set up tool call logging
|
|
260
|
+
setup_tool_callbacks
|
|
261
|
+
|
|
262
|
+
puts '[✓] Chat initialized with tools attached'
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def print_banner
|
|
267
|
+
puts
|
|
268
|
+
puts '=' * 70
|
|
269
|
+
puts 'HTM MCP Client - AI Chat with Memory Tools'
|
|
270
|
+
puts '=' * 70
|
|
271
|
+
puts
|
|
272
|
+
puts "Robot: #{@robot_name}"
|
|
273
|
+
puts "Model: #{OLLAMA_MODEL} (via Ollama)"
|
|
274
|
+
puts "MCP Server: #{MCP_SERVER_PATH}"
|
|
275
|
+
puts
|
|
276
|
+
puts 'Available tools:'
|
|
277
|
+
@tools.each do |tool|
|
|
278
|
+
puts " • #{tool.name}"
|
|
279
|
+
end
|
|
280
|
+
puts
|
|
281
|
+
puts 'Example queries:'
|
|
282
|
+
puts ' "Remember that the API rate limit is 1000 requests per minute"'
|
|
283
|
+
puts ' "What do you know about databases?"'
|
|
284
|
+
puts ' "Show me the memory statistics"'
|
|
285
|
+
puts ' "List all tags"'
|
|
286
|
+
puts ' "Forget node 123"'
|
|
287
|
+
puts
|
|
288
|
+
puts 'Commands:'
|
|
289
|
+
puts ' /tools - List available MCP tools'
|
|
290
|
+
puts ' /resources - List available MCP resources'
|
|
291
|
+
puts ' /stats - Show memory statistics'
|
|
292
|
+
puts ' /tags - List all tags'
|
|
293
|
+
puts ' /clear - Clear chat history'
|
|
294
|
+
puts ' /help - Show this help'
|
|
295
|
+
puts ' /exit - Quit'
|
|
296
|
+
puts
|
|
297
|
+
puts '=' * 70
|
|
298
|
+
puts
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def chat_loop
|
|
303
|
+
loop do
|
|
304
|
+
print 'you> '
|
|
305
|
+
input = gets&.chomp
|
|
306
|
+
break if input.nil?
|
|
307
|
+
|
|
308
|
+
next if input.empty?
|
|
309
|
+
|
|
310
|
+
# Handle commands
|
|
311
|
+
case input
|
|
312
|
+
when '/exit', '/quit', '/q'
|
|
313
|
+
break
|
|
314
|
+
when '/tools'
|
|
315
|
+
show_tools
|
|
316
|
+
next
|
|
317
|
+
when '/resources'
|
|
318
|
+
show_resources
|
|
319
|
+
next
|
|
320
|
+
when '/stats'
|
|
321
|
+
show_stats
|
|
322
|
+
next
|
|
323
|
+
when '/tags'
|
|
324
|
+
show_tags
|
|
325
|
+
next
|
|
326
|
+
when '/clear'
|
|
327
|
+
clear_chat
|
|
328
|
+
next
|
|
329
|
+
when '/help'
|
|
330
|
+
print_banner
|
|
331
|
+
next
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Send to LLM with tools
|
|
335
|
+
begin
|
|
336
|
+
print "\n#{@robot_name}> "
|
|
337
|
+
response = @chat.ask(input)
|
|
338
|
+
puts response.content
|
|
339
|
+
puts
|
|
340
|
+
rescue StandardError => e
|
|
341
|
+
puts "\n[✗] Error: #{e.message}"
|
|
342
|
+
puts " #{e.class}: #{e.backtrace.first}"
|
|
343
|
+
puts
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
puts "\nGoodbye!"
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def show_tools
|
|
352
|
+
puts "\nAvailable MCP Tools:"
|
|
353
|
+
puts '-' * 50
|
|
354
|
+
@tools.each do |tool|
|
|
355
|
+
puts
|
|
356
|
+
puts "#{tool.name}"
|
|
357
|
+
puts " #{tool.description}"
|
|
358
|
+
end
|
|
359
|
+
puts
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def show_resources
|
|
364
|
+
puts "\nAvailable MCP Resources:"
|
|
365
|
+
puts '-' * 50
|
|
366
|
+
begin
|
|
367
|
+
resources = @mcp_client.resources
|
|
368
|
+
if resources.empty?
|
|
369
|
+
puts ' (no resources available)'
|
|
370
|
+
else
|
|
371
|
+
resources.each do |resource|
|
|
372
|
+
puts
|
|
373
|
+
puts "#{resource.uri}"
|
|
374
|
+
puts " #{resource.name}" if resource.respond_to?(:name)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
rescue StandardError => e
|
|
378
|
+
puts " Error fetching resources: #{e.message}"
|
|
379
|
+
end
|
|
380
|
+
puts
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def show_stats
|
|
385
|
+
puts "\nMemory Statistics:"
|
|
386
|
+
puts '-' * 50
|
|
387
|
+
|
|
388
|
+
stats_tool = @tools.find { |t| t.name == 'StatsTool' }
|
|
389
|
+
unless stats_tool
|
|
390
|
+
puts ' StatsTool not available'
|
|
391
|
+
puts
|
|
392
|
+
return
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
result = stats_tool.call({})
|
|
396
|
+
|
|
397
|
+
# Handle different response types from MCP
|
|
398
|
+
# RubyLLM::MCP::Content objects have a .text attribute
|
|
399
|
+
json_str = case result
|
|
400
|
+
when String then result
|
|
401
|
+
when Hash then result.to_json
|
|
402
|
+
else
|
|
403
|
+
# Try to extract text from MCP Content objects
|
|
404
|
+
if result.respond_to?(:text)
|
|
405
|
+
result.text
|
|
406
|
+
elsif result.respond_to?(:content)
|
|
407
|
+
result.content.is_a?(String) ? result.content : result.content.to_s
|
|
408
|
+
else
|
|
409
|
+
result.to_s
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
response = JSON.parse(json_str)
|
|
414
|
+
|
|
415
|
+
if response['success']
|
|
416
|
+
robot = response['current_robot']
|
|
417
|
+
stats = response['statistics']
|
|
418
|
+
|
|
419
|
+
puts
|
|
420
|
+
puts "Current Robot: #{robot['name']} (id=#{robot['id']})"
|
|
421
|
+
puts " Working memory: #{robot['memory_summary']['in_working_memory']} nodes"
|
|
422
|
+
puts " Total nodes: #{robot['memory_summary']['total_nodes']}"
|
|
423
|
+
puts " With embeddings: #{robot['memory_summary']['with_embeddings']}"
|
|
424
|
+
puts
|
|
425
|
+
puts 'Global Statistics:'
|
|
426
|
+
puts " Active nodes: #{stats['nodes']['active']}"
|
|
427
|
+
puts " Deleted nodes: #{stats['nodes']['deleted']}"
|
|
428
|
+
puts " Total tags: #{stats['tags']['total']}"
|
|
429
|
+
puts " Total robots: #{stats['robots']['total']}"
|
|
430
|
+
else
|
|
431
|
+
puts " Error: #{response['error']}"
|
|
432
|
+
end
|
|
433
|
+
puts
|
|
434
|
+
rescue JSON::ParserError => e
|
|
435
|
+
puts " Error parsing response: #{e.message}"
|
|
436
|
+
puts " Raw response: #{result.inspect[0..200]}"
|
|
437
|
+
puts
|
|
438
|
+
rescue StandardError => e
|
|
439
|
+
puts " Error: #{e.message}"
|
|
440
|
+
puts
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def show_tags
|
|
445
|
+
puts "\nTags:"
|
|
446
|
+
puts '-' * 50
|
|
447
|
+
|
|
448
|
+
tags_tool = @tools.find { |t| t.name == 'ListTagsTool' }
|
|
449
|
+
unless tags_tool
|
|
450
|
+
puts ' ListTagsTool not available'
|
|
451
|
+
puts
|
|
452
|
+
return
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
result = tags_tool.call({})
|
|
456
|
+
|
|
457
|
+
# Extract JSON from MCP Content object
|
|
458
|
+
json_str = result.respond_to?(:text) ? result.text : result.to_s
|
|
459
|
+
response = JSON.parse(json_str)
|
|
460
|
+
|
|
461
|
+
if response['success']
|
|
462
|
+
tags = response['tags']
|
|
463
|
+
if tags.empty?
|
|
464
|
+
puts ' (no tags found)'
|
|
465
|
+
else
|
|
466
|
+
tags.each do |tag|
|
|
467
|
+
puts " #{tag['name']} (#{tag['node_count']} nodes)"
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
else
|
|
471
|
+
puts " Error: #{response['error']}"
|
|
472
|
+
end
|
|
473
|
+
puts
|
|
474
|
+
rescue JSON::ParserError => e
|
|
475
|
+
puts " Error parsing response: #{e.message}"
|
|
476
|
+
puts
|
|
477
|
+
rescue StandardError => e
|
|
478
|
+
puts " Error: #{e.message}"
|
|
479
|
+
puts
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def clear_chat
|
|
484
|
+
@chat = RubyLLM.chat(
|
|
485
|
+
model: OLLAMA_MODEL,
|
|
486
|
+
provider: :ollama,
|
|
487
|
+
assume_model_exists: true
|
|
488
|
+
)
|
|
489
|
+
@chat.with_tools(*@tools)
|
|
490
|
+
setup_tool_callbacks
|
|
491
|
+
|
|
492
|
+
puts '[✓] Chat history cleared'
|
|
493
|
+
puts
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def setup_tool_callbacks
|
|
498
|
+
@chat.on_tool_call do |tool_call|
|
|
499
|
+
puts "\n[Tool Call] #{tool_call.name}"
|
|
500
|
+
puts " Arguments: #{tool_call.arguments.inspect}"
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
@chat.on_tool_result do |tool_call, result|
|
|
504
|
+
# tool_call may be a Content object, so safely get the name
|
|
505
|
+
tool_name = tool_call.respond_to?(:name) ? tool_call.name : tool_call.class.name.split('::').last
|
|
506
|
+
puts "[Tool Result] #{tool_name}"
|
|
507
|
+
display_result = result.to_s
|
|
508
|
+
display_result = display_result[0..200] + '...' if display_result.length > 200
|
|
509
|
+
puts " Result: #{display_result}"
|
|
510
|
+
puts
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def cleanup
|
|
516
|
+
@mcp_client&.close if @mcp_client.respond_to?(:close)
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Main entry point
|
|
521
|
+
begin
|
|
522
|
+
require 'ruby_llm/mcp'
|
|
523
|
+
rescue LoadError
|
|
524
|
+
warn 'Error: ruby_llm-mcp gem not found.'
|
|
525
|
+
warn 'Install it with: gem install ruby_llm-mcp'
|
|
526
|
+
exit 1
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
HTMMcpClient.new.run
|
data/examples/sinatra_app/app.rb
CHANGED
|
@@ -281,7 +281,7 @@ __END__
|
|
|
281
281
|
</head>
|
|
282
282
|
<body>
|
|
283
283
|
<h1>HTM Sinatra Example</h1>
|
|
284
|
-
<p>Hierarchical
|
|
284
|
+
<p>Hierarchical Temporal Memory with tag-enhanced hybrid search and Sidekiq background jobs</p>
|
|
285
285
|
|
|
286
286
|
<div class="section">
|
|
287
287
|
<h2>Remember Information</h2>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# HTM Telemetry Demo
|
|
2
|
+
|
|
3
|
+
This demo shows HTM metrics in a **live Grafana dashboard** using locally installed Prometheus and Grafana via Homebrew.
|
|
4
|
+
|
|
5
|
+
> **First time setup?** See [SETUP_README.md](SETUP_README.md) for detailed installation and configuration instructions.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 1. Install Prometheus and Grafana via Homebrew
|
|
11
|
+
brew install prometheus grafana
|
|
12
|
+
|
|
13
|
+
# 2. Install required Ruby gems
|
|
14
|
+
gem install prometheus-client webrick
|
|
15
|
+
|
|
16
|
+
# 3. Run the demo
|
|
17
|
+
cd examples/telemetry
|
|
18
|
+
ruby demo.rb
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The demo will automatically:
|
|
22
|
+
- Check that Prometheus and Grafana are installed
|
|
23
|
+
- Start both services if not already running
|
|
24
|
+
- Configure Prometheus to scrape the demo's metrics
|
|
25
|
+
- Clean up any previous demo data (hard delete)
|
|
26
|
+
- Open Grafana in your browser
|
|
27
|
+
- Run HTM operations and export live metrics
|
|
28
|
+
|
|
29
|
+
## What You'll See
|
|
30
|
+
|
|
31
|
+
Once running, open Grafana at http://localhost:3000 (login: admin/admin) and import the dashboard.
|
|
32
|
+
|
|
33
|
+
### Dashboard Panels
|
|
34
|
+
|
|
35
|
+
| Panel | Description |
|
|
36
|
+
|-------|-------------|
|
|
37
|
+
| **Total Successful Jobs** | Count of completed embedding and tag jobs |
|
|
38
|
+
| **Total Failed Jobs** | Count of failed jobs |
|
|
39
|
+
| **Cache Hit Rate** | Percentage of queries served from cache |
|
|
40
|
+
| **LLM Job Latency (p95)** | 95th percentile latency for embedding/tag generation |
|
|
41
|
+
| **Search Latency by Strategy** | p95 latency for vector, fulltext, hybrid search |
|
|
42
|
+
| **Jobs per Minute** | Throughput by job type |
|
|
43
|
+
| **Cache Operations** | Hit/miss rate over time |
|
|
44
|
+
|
|
45
|
+
## Importing the Dashboard
|
|
46
|
+
|
|
47
|
+
1. Open Grafana: http://localhost:3000
|
|
48
|
+
2. Go to: **Dashboards** → **Import**
|
|
49
|
+
3. Click "Upload JSON file"
|
|
50
|
+
4. Select: `examples/telemetry/grafana/dashboards/htm-metrics.json`
|
|
51
|
+
5. Select your Prometheus datasource
|
|
52
|
+
6. Click **Import**
|
|
53
|
+
|
|
54
|
+
If you don't have a Prometheus datasource configured:
|
|
55
|
+
1. Go to: **Connections** → **Data sources** → **Add data source**
|
|
56
|
+
2. Select **Prometheus**
|
|
57
|
+
3. URL: `http://localhost:9090`
|
|
58
|
+
4. Click **Save & test**
|
|
59
|
+
|
|
60
|
+
## Architecture
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
┌─────────────┐ ┌─────────────────┐
|
|
64
|
+
│ demo.rb │ ──── scrapes ────▶ │ Prometheus │
|
|
65
|
+
│ (metrics │ :9394 │ :9090 │
|
|
66
|
+
│ server) │ └────────┬────────┘
|
|
67
|
+
└─────────────┘ │
|
|
68
|
+
│ PromQL queries
|
|
69
|
+
│ │
|
|
70
|
+
▼ ▼
|
|
71
|
+
┌─────────────┐ ┌─────────────────┐
|
|
72
|
+
│ HTM │ │ Grafana │
|
|
73
|
+
│ operations │ │ :3000 │
|
|
74
|
+
└─────────────┘ └─────────────────┘
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Available Metrics
|
|
78
|
+
|
|
79
|
+
| Metric | Type | Labels | Description |
|
|
80
|
+
|--------|------|--------|-------------|
|
|
81
|
+
| `htm_jobs_total` | Counter | job, status | Job execution count |
|
|
82
|
+
| `htm_embedding_latency_milliseconds` | Histogram | provider, status | Embedding time |
|
|
83
|
+
| `htm_tag_latency_milliseconds` | Histogram | provider, status | Tag extraction time |
|
|
84
|
+
| `htm_search_latency_milliseconds` | Histogram | strategy | Search operation time |
|
|
85
|
+
| `htm_cache_operations_total` | Counter | operation | Cache hit/miss count |
|
|
86
|
+
|
|
87
|
+
## Endpoints
|
|
88
|
+
|
|
89
|
+
| Service | URL | Purpose |
|
|
90
|
+
|---------|-----|---------|
|
|
91
|
+
| Demo Metrics | http://localhost:9394/metrics | Raw Prometheus metrics |
|
|
92
|
+
| Prometheus | http://localhost:9090 | Metrics storage & queries |
|
|
93
|
+
| Grafana | http://localhost:3000 | Visualization (admin/admin) |
|
|
94
|
+
|
|
95
|
+
## Stopping Services
|
|
96
|
+
|
|
97
|
+
The demo leaves Prometheus and Grafana running for convenience. To stop them:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
brew services stop prometheus grafana
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Troubleshooting
|
|
104
|
+
|
|
105
|
+
### No metrics in Grafana
|
|
106
|
+
|
|
107
|
+
1. Verify the demo is running and exposing metrics:
|
|
108
|
+
```bash
|
|
109
|
+
curl http://localhost:9394/metrics
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
2. Check Prometheus is scraping:
|
|
113
|
+
- Open http://localhost:9090/targets
|
|
114
|
+
- Look for `htm-demo` target with state "UP"
|
|
115
|
+
|
|
116
|
+
3. If target is missing, check Prometheus config:
|
|
117
|
+
```bash
|
|
118
|
+
cat /opt/homebrew/etc/prometheus.yml
|
|
119
|
+
# Should include htm-demo job
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Port already in use
|
|
123
|
+
|
|
124
|
+
If port 9394 is busy, edit `demo.rb` and change `METRICS_PORT`.
|
|
125
|
+
|
|
126
|
+
### Services won't start
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Check service status
|
|
130
|
+
brew services list
|
|
131
|
+
|
|
132
|
+
# View logs
|
|
133
|
+
brew services info prometheus
|
|
134
|
+
brew services info grafana
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Files
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
examples/telemetry/
|
|
141
|
+
├── README.md # This file
|
|
142
|
+
├── SETUP_README.md # Detailed setup instructions
|
|
143
|
+
├── demo.rb # Main demo script
|
|
144
|
+
└── grafana/
|
|
145
|
+
└── dashboards/
|
|
146
|
+
└── htm-metrics.json # Import this into Grafana
|
|
147
|
+
```
|