htm 0.0.2 → 0.0.11
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/.aigcm_msg +1 -0
- data/.architecture/reviews/comprehensive-codebase-review.md +577 -0
- data/.claude/settings.local.json +95 -0
- data/.irbrc +283 -80
- data/.tbls.yml +2 -1
- data/CHANGELOG.md +327 -26
- data/CLAUDE.md +603 -0
- data/README.md +83 -12
- data/Rakefile +5 -0
- data/bin/htm_mcp.rb +527 -0
- data/db/migrate/{20250101000001_enable_extensions.rb → 00001_enable_extensions.rb} +0 -1
- data/db/migrate/00002_create_robots.rb +11 -0
- data/db/migrate/00003_create_file_sources.rb +20 -0
- data/db/migrate/00004_create_nodes.rb +65 -0
- data/db/migrate/00005_create_tags.rb +13 -0
- data/db/migrate/00006_create_node_tags.rb +18 -0
- data/db/migrate/00007_create_robot_nodes.rb +26 -0
- data/db/migrate/00009_add_working_memory_to_robot_nodes.rb +12 -0
- data/db/schema.sql +172 -1
- data/docs/api/database.md +1 -2
- data/docs/api/htm.md +197 -2
- data/docs/api/yard/HTM/ActiveRecordConfig.md +23 -0
- data/docs/api/yard/HTM/AuthorizationError.md +11 -0
- data/docs/api/yard/HTM/CircuitBreaker.md +92 -0
- data/docs/api/yard/HTM/CircuitBreakerOpenError.md +34 -0
- data/docs/api/yard/HTM/Configuration.md +175 -0
- data/docs/api/yard/HTM/Database.md +99 -0
- data/docs/api/yard/HTM/DatabaseError.md +14 -0
- data/docs/api/yard/HTM/EmbeddingError.md +18 -0
- data/docs/api/yard/HTM/EmbeddingService.md +58 -0
- data/docs/api/yard/HTM/Error.md +11 -0
- data/docs/api/yard/HTM/JobAdapter.md +39 -0
- data/docs/api/yard/HTM/LongTermMemory.md +342 -0
- data/docs/api/yard/HTM/NotFoundError.md +17 -0
- data/docs/api/yard/HTM/Observability.md +107 -0
- data/docs/api/yard/HTM/QueryTimeoutError.md +19 -0
- data/docs/api/yard/HTM/Railtie.md +27 -0
- data/docs/api/yard/HTM/ResourceExhaustedError.md +13 -0
- data/docs/api/yard/HTM/TagError.md +18 -0
- data/docs/api/yard/HTM/TagService.md +67 -0
- data/docs/api/yard/HTM/Timeframe/Result.md +24 -0
- data/docs/api/yard/HTM/Timeframe.md +40 -0
- data/docs/api/yard/HTM/TimeframeExtractor/Result.md +24 -0
- data/docs/api/yard/HTM/TimeframeExtractor.md +45 -0
- data/docs/api/yard/HTM/ValidationError.md +20 -0
- data/docs/api/yard/HTM/WorkingMemory.md +131 -0
- data/docs/api/yard/HTM.md +80 -0
- data/docs/api/yard/index.csv +179 -0
- data/docs/api/yard-reference.md +51 -0
- data/docs/database/README.md +128 -128
- data/docs/database/public.file_sources.md +42 -0
- data/docs/database/public.file_sources.svg +211 -0
- data/docs/database/public.node_tags.md +4 -4
- data/docs/database/public.node_tags.svg +212 -79
- data/docs/database/public.nodes.md +22 -12
- data/docs/database/public.nodes.svg +246 -127
- data/docs/database/public.robot_nodes.md +11 -9
- data/docs/database/public.robot_nodes.svg +220 -98
- data/docs/database/public.robots.md +2 -2
- data/docs/database/public.robots.svg +136 -81
- data/docs/database/public.tags.md +3 -3
- data/docs/database/public.tags.svg +118 -39
- data/docs/database/schema.json +850 -771
- data/docs/database/schema.svg +256 -197
- data/docs/development/schema.md +67 -2
- data/docs/guides/adding-memories.md +93 -7
- data/docs/guides/recalling-memories.md +36 -1
- data/examples/README.md +405 -0
- data/examples/cli_app/htm_cli.rb +65 -5
- data/examples/cli_app/temp.log +93 -0
- data/examples/file_loader_usage.rb +177 -0
- data/examples/mcp_client.rb +529 -0
- data/examples/robot_groups/lib/robot_group.rb +419 -0
- data/examples/robot_groups/lib/working_memory_channel.rb +140 -0
- data/examples/robot_groups/multi_process.rb +286 -0
- data/examples/robot_groups/robot_worker.rb +136 -0
- data/examples/robot_groups/same_process.rb +229 -0
- data/examples/timeframe_demo.rb +276 -0
- data/lib/htm/active_record_config.rb +1 -1
- data/lib/htm/circuit_breaker.rb +202 -0
- data/lib/htm/configuration.rb +59 -13
- data/lib/htm/database.rb +67 -36
- data/lib/htm/embedding_service.rb +39 -2
- data/lib/htm/errors.rb +131 -11
- data/lib/htm/jobs/generate_embedding_job.rb +5 -4
- data/lib/htm/jobs/generate_tags_job.rb +4 -0
- data/lib/htm/loaders/markdown_loader.rb +263 -0
- data/lib/htm/loaders/paragraph_chunker.rb +112 -0
- data/lib/htm/long_term_memory.rb +460 -343
- data/lib/htm/models/file_source.rb +99 -0
- data/lib/htm/models/node.rb +80 -5
- data/lib/htm/models/robot.rb +24 -1
- data/lib/htm/models/robot_node.rb +1 -0
- data/lib/htm/models/tag.rb +254 -4
- data/lib/htm/observability.rb +395 -0
- data/lib/htm/tag_service.rb +60 -3
- data/lib/htm/tasks.rb +26 -1
- data/lib/htm/timeframe.rb +194 -0
- data/lib/htm/timeframe_extractor.rb +307 -0
- data/lib/htm/version.rb +1 -1
- data/lib/htm/working_memory.rb +165 -70
- data/lib/htm.rb +328 -130
- data/lib/tasks/doc.rake +300 -0
- data/lib/tasks/files.rake +299 -0
- data/lib/tasks/htm.rake +158 -3
- data/lib/tasks/jobs.rake +3 -9
- data/lib/tasks/tags.rake +166 -6
- data/mkdocs.yml +36 -1
- data/notes/ARCHITECTURE_REVIEW.md +1167 -0
- data/notes/IMPLEMENTATION_SUMMARY.md +606 -0
- data/notes/MULTI_FRAMEWORK_IMPLEMENTATION.md +451 -0
- data/notes/next_steps.md +100 -0
- data/notes/plan.md +627 -0
- data/notes/tag_ontology_enhancement_ideas.md +222 -0
- data/notes/timescaledb_removal_summary.md +200 -0
- metadata +158 -17
- data/db/migrate/20250101000002_create_robots.rb +0 -14
- data/db/migrate/20250101000003_create_nodes.rb +0 -42
- data/db/migrate/20250101000005_create_tags.rb +0 -38
- data/db/migrate/20250101000007_add_node_vector_indexes.rb +0 -30
- data/db/migrate/20250125000001_add_content_hash_to_nodes.rb +0 -14
- data/db/migrate/20250125000002_create_robot_nodes.rb +0 -35
- data/db/migrate/20250125000003_remove_source_and_robot_id_from_nodes.rb +0 -28
- data/db/migrate/20250126000001_create_working_memories.rb +0 -19
- data/db/migrate/20250126000002_remove_unused_columns.rb +0 -12
- data/docs/database/public.working_memories.md +0 -40
- data/docs/database/public.working_memories.svg +0 -112
- data/lib/htm/models/working_memory_entry.rb +0 -88
data/README.md
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
- **Client-Side Embeddings**
|
|
29
29
|
- Automatic embedding generation before database insertion
|
|
30
|
-
-
|
|
30
|
+
- Uses the [ruby_llm](https://ruby_llm.com) gem for LLM access
|
|
31
31
|
- Configurable embedding providers and models
|
|
32
32
|
|
|
33
33
|
- **Two-Tier Memory Architecture**
|
|
@@ -62,6 +62,12 @@
|
|
|
62
62
|
- Tag-based categorization
|
|
63
63
|
- Hierarchical tag structures
|
|
64
64
|
|
|
65
|
+
- **File Loading**
|
|
66
|
+
- Load markdown files into long-term memory
|
|
67
|
+
- Automatic paragraph-based chunking
|
|
68
|
+
- Source file tracking with re-sync support
|
|
69
|
+
- YAML frontmatter extraction as metadata
|
|
70
|
+
|
|
65
71
|
## Installation
|
|
66
72
|
|
|
67
73
|
Add this line to your application's Gemfile:
|
|
@@ -264,6 +270,42 @@ memories = htm.recall(
|
|
|
264
270
|
htm.forget(node_id, confirm: :confirmed)
|
|
265
271
|
```
|
|
266
272
|
|
|
273
|
+
### Loading Files
|
|
274
|
+
|
|
275
|
+
HTM can load text-based files (currently markdown) into long-term memory with automatic chunking and source tracking.
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
htm = HTM.new(robot_name: "Document Loader")
|
|
279
|
+
|
|
280
|
+
# Load a single markdown file
|
|
281
|
+
result = htm.load_file("docs/guide.md")
|
|
282
|
+
# => { file_source_id: 1, chunks_created: 5, chunks_updated: 0, chunks_deleted: 0 }
|
|
283
|
+
|
|
284
|
+
# Load all markdown files in a directory
|
|
285
|
+
results = htm.load_directory("docs/", pattern: "**/*.md")
|
|
286
|
+
|
|
287
|
+
# Get nodes from a specific file
|
|
288
|
+
nodes = htm.nodes_from_file("docs/guide.md")
|
|
289
|
+
|
|
290
|
+
# Unload a file (soft deletes chunks)
|
|
291
|
+
htm.unload_file("docs/guide.md")
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Features:**
|
|
295
|
+
- **Paragraph chunking**: Text split by blank lines, code blocks preserved
|
|
296
|
+
- **Source tracking**: Files tracked with mtime for automatic re-sync
|
|
297
|
+
- **YAML frontmatter**: Extracted and stored as metadata
|
|
298
|
+
- **Duplicate detection**: Content hash prevents duplicate nodes
|
|
299
|
+
|
|
300
|
+
**Rake tasks:**
|
|
301
|
+
```bash
|
|
302
|
+
rake 'htm:files:load[docs/guide.md]' # Load a single file
|
|
303
|
+
rake 'htm:files:load_dir[docs/]' # Load all markdown files from directory
|
|
304
|
+
rake htm:files:list # List all loaded file sources
|
|
305
|
+
rake htm:files:sync # Sync all files (reload changed)
|
|
306
|
+
rake htm:files:stats # Show file loading statistics
|
|
307
|
+
```
|
|
308
|
+
|
|
267
309
|
### Automatic Tag Extraction
|
|
268
310
|
|
|
269
311
|
HTM automatically extracts hierarchical tags from content using LLM analysis. Tags are inferred from the content itself - you never specify them manually.
|
|
@@ -518,13 +560,14 @@ HTM provides a minimal, focused API with only 3 core instance methods for memory
|
|
|
518
560
|
|
|
519
561
|
### Core Memory Operations
|
|
520
562
|
|
|
521
|
-
#### `remember(content, source: "")`
|
|
563
|
+
#### `remember(content, source: "", metadata: {})`
|
|
522
564
|
|
|
523
565
|
Store information in memory. Embeddings and tags are automatically generated asynchronously.
|
|
524
566
|
|
|
525
567
|
**Parameters:**
|
|
526
568
|
- `content` (String, required) - The information to remember. Converted to string if nil. Returns ID of last node if empty.
|
|
527
569
|
- `source` (String, optional) - Where the content came from (e.g., "user", "assistant", "system"). Defaults to empty string.
|
|
570
|
+
- `metadata` (Hash, optional) - Arbitrary key-value metadata stored as JSONB. Keys must be strings or symbols. Defaults to `{}`.
|
|
528
571
|
|
|
529
572
|
**Returns:** Integer - The node ID of the stored memory
|
|
530
573
|
|
|
@@ -536,6 +579,12 @@ node_id = htm.remember("PostgreSQL is excellent for vector search with pgvector"
|
|
|
536
579
|
# Store without source (uses default empty string)
|
|
537
580
|
node_id = htm.remember("HTM uses two-tier memory architecture")
|
|
538
581
|
|
|
582
|
+
# Store with metadata
|
|
583
|
+
node_id = htm.remember(
|
|
584
|
+
"User prefers dark mode",
|
|
585
|
+
metadata: { category: "preference", priority: "high", version: 2 }
|
|
586
|
+
)
|
|
587
|
+
|
|
539
588
|
# Nil/empty handling
|
|
540
589
|
node_id = htm.remember(nil) # Returns ID of last node without creating duplicate
|
|
541
590
|
node_id = htm.remember("") # Returns ID of last node without creating duplicate
|
|
@@ -543,7 +592,7 @@ node_id = htm.remember("") # Returns ID of last node without creating duplicat
|
|
|
543
592
|
|
|
544
593
|
---
|
|
545
594
|
|
|
546
|
-
#### `recall(topic, timeframe: nil, limit: 20, strategy: :vector, with_relevance: false, query_tags: [])`
|
|
595
|
+
#### `recall(topic, timeframe: nil, limit: 20, strategy: :vector, with_relevance: false, query_tags: [], metadata: {})`
|
|
547
596
|
|
|
548
597
|
Retrieve memories using temporal filtering and semantic/keyword search.
|
|
549
598
|
|
|
@@ -559,8 +608,9 @@ Retrieve memories using temporal filtering and semantic/keyword search.
|
|
|
559
608
|
- `:hybrid` - Weighted combination (70% vector, 30% full-text)
|
|
560
609
|
- `with_relevance` (Boolean, optional) - Include dynamic relevance scores. Default: false
|
|
561
610
|
- `query_tags` (Array<String>, optional) - Filter results by tags. Default: []
|
|
611
|
+
- `metadata` (Hash, optional) - Filter results by metadata using JSONB containment (`@>`). Default: `{}`
|
|
562
612
|
|
|
563
|
-
**Returns:** Array<Hash> - Matching memories with fields: `id`, `content`, `source`, `created_at`, `access_count`, (optionally `relevance`)
|
|
613
|
+
**Returns:** Array<Hash> - Matching memories with fields: `id`, `content`, `source`, `created_at`, `access_count`, `metadata`, (optionally `relevance`)
|
|
564
614
|
|
|
565
615
|
**Example:**
|
|
566
616
|
```ruby
|
|
@@ -586,6 +636,21 @@ memories = htm.recall(
|
|
|
586
636
|
query_tags: ["architecture"]
|
|
587
637
|
)
|
|
588
638
|
# => [{ "id" => 123, "content" => "...", "relevance" => 0.92, ... }, ...]
|
|
639
|
+
|
|
640
|
+
# Filter by metadata
|
|
641
|
+
memories = htm.recall(
|
|
642
|
+
"user preferences",
|
|
643
|
+
metadata: { category: "preference" }
|
|
644
|
+
)
|
|
645
|
+
# => Returns only nodes with metadata containing { category: "preference" }
|
|
646
|
+
|
|
647
|
+
# Combine metadata with other filters
|
|
648
|
+
memories = htm.recall(
|
|
649
|
+
"settings",
|
|
650
|
+
timeframe: "last month",
|
|
651
|
+
strategy: :hybrid,
|
|
652
|
+
metadata: { priority: "high", version: 2 }
|
|
653
|
+
)
|
|
589
654
|
```
|
|
590
655
|
|
|
591
656
|
---
|
|
@@ -1312,10 +1377,16 @@ See [htm_teamwork.md](htm_teamwork.md) for detailed design documentation and pla
|
|
|
1312
1377
|
### Database Schema
|
|
1313
1378
|
|
|
1314
1379
|
- `robots`: Robot registry for all LLM agents using HTM
|
|
1315
|
-
- `nodes`: Main memory storage with vector embeddings (pgvector), full-text search (tsvector), metadata
|
|
1380
|
+
- `nodes`: Main memory storage with vector embeddings (pgvector), full-text search (tsvector), JSONB metadata
|
|
1316
1381
|
- `tags`: Hierarchical tag ontology (format: `root:level1:level2:level3`)
|
|
1317
1382
|
- `node_tags`: Join table implementing many-to-many relationship between nodes and tags
|
|
1318
1383
|
|
|
1384
|
+
**Nodes Table Key Columns:**
|
|
1385
|
+
- `content`: The memory content
|
|
1386
|
+
- `embedding`: Vector embedding for semantic search (up to 2000 dimensions)
|
|
1387
|
+
- `metadata`: JSONB column for arbitrary key-value data (filterable via `@>` containment operator)
|
|
1388
|
+
- `content_hash`: SHA-256 hash for deduplication
|
|
1389
|
+
|
|
1319
1390
|
### Service Architecture
|
|
1320
1391
|
|
|
1321
1392
|
HTM uses a layered architecture for LLM integration:
|
|
@@ -1329,13 +1400,13 @@ This separation allows you to provide any LLM implementation while HTM handles r
|
|
|
1329
1400
|
## Roadmap
|
|
1330
1401
|
|
|
1331
1402
|
- [x] Phase 1: Foundation (basic two-tier memory)
|
|
1332
|
-
- [
|
|
1333
|
-
- [
|
|
1334
|
-
- [
|
|
1335
|
-
- [
|
|
1336
|
-
- [
|
|
1337
|
-
- [
|
|
1338
|
-
- [
|
|
1403
|
+
- [x] Phase 2: RAG retrieval (semantic search)
|
|
1404
|
+
- [x] Phase 3: Relationships & tags
|
|
1405
|
+
- [x] Phase 4: Working memory management
|
|
1406
|
+
- [x] Phase 5: Hive mind features
|
|
1407
|
+
- [x] Phase 6: Operations & observability
|
|
1408
|
+
- [x] Phase 7: Advanced features
|
|
1409
|
+
- [x] Phase 8: Production-ready gem
|
|
1339
1410
|
|
|
1340
1411
|
|
|
1341
1412
|
## Contributing
|
data/Rakefile
CHANGED
data/bin/htm_mcp.rb
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
# htm/bin/htm_mcp.rb
|
|
4
|
+
|
|
5
|
+
# MCP Server Example for HTM
|
|
6
|
+
#
|
|
7
|
+
# This example demonstrates using FastMCP to expose HTM's memory
|
|
8
|
+
# capabilities as an MCP (Model Context Protocol) server that can
|
|
9
|
+
# be used by AI assistants like Claude Desktop.
|
|
10
|
+
#
|
|
11
|
+
# Prerequisites:
|
|
12
|
+
# 1. Install fast-mcp gem: gem install fast-mcp
|
|
13
|
+
# 2. Set HTM_DBURL environment variable
|
|
14
|
+
# 3. Initialize database schema: rake db_setup
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# ruby examples/mcp_server.rb
|
|
18
|
+
#
|
|
19
|
+
# The server uses STDIO transport by default, making it compatible
|
|
20
|
+
# with Claude Desktop and other MCP clients.
|
|
21
|
+
#
|
|
22
|
+
# Claude Desktop configuration (~/.config/claude/claude_desktop_config.json):
|
|
23
|
+
# {
|
|
24
|
+
# "mcpServers": {
|
|
25
|
+
# "htm-memory": {
|
|
26
|
+
# "command": "ruby",
|
|
27
|
+
# "args": ["/path/to/htm/examples/mcp_server.rb"],
|
|
28
|
+
# "env": {
|
|
29
|
+
# "HTM_DBURL": "postgresql://postgres@localhost:5432/htm_development"
|
|
30
|
+
# }
|
|
31
|
+
# }
|
|
32
|
+
# }
|
|
33
|
+
# }
|
|
34
|
+
|
|
35
|
+
require_relative '../lib/htm'
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
require 'fast_mcp'
|
|
39
|
+
rescue LoadError
|
|
40
|
+
warn "Error: fast-mcp gem not found."
|
|
41
|
+
warn "Install it with: gem install fast-mcp"
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check environment
|
|
46
|
+
unless ENV['HTM_DBURL']
|
|
47
|
+
warn "Error: HTM_DBURL not set."
|
|
48
|
+
warn " export HTM_DBURL=\"postgresql://postgres@localhost:5432/htm_development\""
|
|
49
|
+
exit 1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# IMPORTANT: MCP uses STDIO for JSON-RPC communication.
|
|
53
|
+
# ALL logging must go to STDERR to avoid corrupting the protocol.
|
|
54
|
+
# Create a logger that writes to STDERR for MCP server diagnostics.
|
|
55
|
+
MCP_STDERR_LOG = Logger.new($stderr)
|
|
56
|
+
MCP_STDERR_LOG.level = Logger::INFO
|
|
57
|
+
MCP_STDERR_LOG.formatter = proc do |severity, datetime, _progname, msg|
|
|
58
|
+
"[MCP #{severity}] #{datetime.strftime('%H:%M:%S')} #{msg}\n"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Silent logger for RubyLLM/HTM internals (prevents STDOUT corruption)
|
|
62
|
+
mcp_logger = Logger.new(IO::NULL)
|
|
63
|
+
|
|
64
|
+
# Configure RubyLLM to not log to STDOUT (corrupts MCP protocol)
|
|
65
|
+
require 'ruby_llm'
|
|
66
|
+
RubyLLM.configure do |config|
|
|
67
|
+
config.logger = mcp_logger
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Configure HTM
|
|
71
|
+
HTM.configure do |config|
|
|
72
|
+
config.job_backend = :inline # Synchronous for MCP responses
|
|
73
|
+
config.logger = mcp_logger # Silent logging for MCP
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Session state for the current robot
|
|
77
|
+
# Each MCP client spawns its own server process, so this is naturally isolated
|
|
78
|
+
module MCPSession
|
|
79
|
+
DEFAULT_ROBOT_NAME = "mcp_default"
|
|
80
|
+
|
|
81
|
+
class << self
|
|
82
|
+
def htm_instance
|
|
83
|
+
@htm_instance ||= HTM.new(robot_name: DEFAULT_ROBOT_NAME)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def set_robot(name)
|
|
87
|
+
@robot_name = name
|
|
88
|
+
@htm_instance = HTM.new(robot_name: name)
|
|
89
|
+
MCP_STDERR_LOG.info "Robot set: #{name} (id=#{@htm_instance.robot_id})"
|
|
90
|
+
@htm_instance
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def robot_name
|
|
94
|
+
@robot_name || DEFAULT_ROBOT_NAME
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def robot_initialized?
|
|
98
|
+
@robot_name != nil
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Tool: Set the robot identity for this session
|
|
104
|
+
class SetRobotTool < FastMcp::Tool
|
|
105
|
+
description "Set the robot identity for this session. Call this first to establish your robot name."
|
|
106
|
+
|
|
107
|
+
arguments do
|
|
108
|
+
required(:name).filled(:string).description("The robot name (will be created if it doesn't exist)")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def call(name:)
|
|
112
|
+
MCP_STDERR_LOG.info "SetRobotTool called: name=#{name.inspect}"
|
|
113
|
+
|
|
114
|
+
htm = MCPSession.set_robot(name)
|
|
115
|
+
robot = HTM::Models::Robot.find(htm.robot_id)
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
success: true,
|
|
119
|
+
robot_id: htm.robot_id,
|
|
120
|
+
robot_name: htm.robot_name,
|
|
121
|
+
node_count: robot.node_count,
|
|
122
|
+
message: "Robot '#{name}' is now active for this session"
|
|
123
|
+
}.to_json
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Tool: Get current robot info
|
|
128
|
+
class GetRobotTool < FastMcp::Tool
|
|
129
|
+
description "Get information about the current robot for this session"
|
|
130
|
+
|
|
131
|
+
arguments do
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def call
|
|
135
|
+
MCP_STDERR_LOG.info "GetRobotTool called"
|
|
136
|
+
|
|
137
|
+
htm = MCPSession.htm_instance
|
|
138
|
+
robot = HTM::Models::Robot.find(htm.robot_id)
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
success: true,
|
|
142
|
+
robot_id: htm.robot_id,
|
|
143
|
+
robot_name: htm.robot_name,
|
|
144
|
+
initialized: MCPSession.robot_initialized?,
|
|
145
|
+
memory_summary: robot.memory_summary
|
|
146
|
+
}.to_json
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Tool: Get working memory contents for session restore
|
|
151
|
+
class GetWorkingMemoryTool < FastMcp::Tool
|
|
152
|
+
description "Get all working memory contents for the current robot. Use this to restore a previous session."
|
|
153
|
+
|
|
154
|
+
arguments do
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def call
|
|
158
|
+
htm = MCPSession.htm_instance
|
|
159
|
+
robot = HTM::Models::Robot.find(htm.robot_id)
|
|
160
|
+
MCP_STDERR_LOG.info "GetWorkingMemoryTool called for robot=#{htm.robot_name}"
|
|
161
|
+
|
|
162
|
+
# Get all nodes in working memory with their metadata
|
|
163
|
+
# Filter out any robot_nodes where the node has been deleted (node uses default_scope)
|
|
164
|
+
working_memory_nodes = robot.robot_nodes
|
|
165
|
+
.in_working_memory
|
|
166
|
+
.joins(:node) # Inner join excludes deleted nodes
|
|
167
|
+
.includes(node: :tags)
|
|
168
|
+
.order(last_remembered_at: :desc)
|
|
169
|
+
.filter_map do |rn|
|
|
170
|
+
next unless rn.node # Extra safety check
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
id: rn.node.id,
|
|
174
|
+
content: rn.node.content,
|
|
175
|
+
tags: rn.node.tags.map(&:name),
|
|
176
|
+
remember_count: rn.remember_count,
|
|
177
|
+
last_remembered_at: rn.last_remembered_at&.iso8601,
|
|
178
|
+
created_at: rn.node.created_at.iso8601
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
MCP_STDERR_LOG.info "GetWorkingMemoryTool complete: #{working_memory_nodes.length} nodes in working memory"
|
|
183
|
+
|
|
184
|
+
{
|
|
185
|
+
success: true,
|
|
186
|
+
robot_id: htm.robot_id,
|
|
187
|
+
robot_name: htm.robot_name,
|
|
188
|
+
count: working_memory_nodes.length,
|
|
189
|
+
working_memory: working_memory_nodes
|
|
190
|
+
}.to_json
|
|
191
|
+
rescue StandardError => e
|
|
192
|
+
MCP_STDERR_LOG.error "GetWorkingMemoryTool error: #{e.message}"
|
|
193
|
+
{ success: false, error: e.message, count: 0, working_memory: [] }.to_json
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Tool: Remember information
|
|
198
|
+
class RememberTool < FastMcp::Tool
|
|
199
|
+
description "Store information in HTM long-term memory with optional tags"
|
|
200
|
+
|
|
201
|
+
arguments do
|
|
202
|
+
required(:content).filled(:string).description("The content to remember")
|
|
203
|
+
optional(:tags).array(:string).description("Optional tags for categorization (e.g., ['database:postgresql', 'config'])")
|
|
204
|
+
optional(:metadata).hash.description("Optional metadata key-value pairs")
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def call(content:, tags: [], metadata: {})
|
|
208
|
+
MCP_STDERR_LOG.info "RememberTool called: content=#{content[0..50].inspect}..."
|
|
209
|
+
|
|
210
|
+
htm = MCPSession.htm_instance
|
|
211
|
+
node_id = htm.remember(content, tags: tags, metadata: metadata)
|
|
212
|
+
node = HTM::Models::Node.includes(:tags).find(node_id)
|
|
213
|
+
|
|
214
|
+
MCP_STDERR_LOG.info "Memory stored: node_id=#{node_id}, robot=#{htm.robot_name}, tags=#{node.tags.map(&:name)}"
|
|
215
|
+
|
|
216
|
+
{
|
|
217
|
+
success: true,
|
|
218
|
+
node_id: node_id,
|
|
219
|
+
robot_id: htm.robot_id,
|
|
220
|
+
robot_name: htm.robot_name,
|
|
221
|
+
content: node.content,
|
|
222
|
+
tags: node.tags.map(&:name),
|
|
223
|
+
created_at: node.created_at.iso8601
|
|
224
|
+
}.to_json
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Tool: Recall memories
|
|
229
|
+
class RecallTool < FastMcp::Tool
|
|
230
|
+
description "Search and retrieve memories from HTM using semantic, full-text, or hybrid search"
|
|
231
|
+
|
|
232
|
+
arguments do
|
|
233
|
+
required(:query).filled(:string).description("Search query - can be natural language or keywords")
|
|
234
|
+
optional(:limit).filled(:integer).description("Maximum number of results (default: 10)")
|
|
235
|
+
optional(:strategy).filled(:string).description("Search strategy: 'vector', 'fulltext', or 'hybrid' (default: 'hybrid')")
|
|
236
|
+
optional(:timeframe).filled(:string).description("Filter by time: 'today', 'this week', 'this month', or ISO8601 date range")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def call(query:, limit: 10, strategy: 'hybrid', timeframe: nil)
|
|
240
|
+
htm = MCPSession.htm_instance
|
|
241
|
+
MCP_STDERR_LOG.info "RecallTool called: query=#{query.inspect}, strategy=#{strategy}, limit=#{limit}, robot=#{htm.robot_name}"
|
|
242
|
+
|
|
243
|
+
recall_opts = {
|
|
244
|
+
limit: limit,
|
|
245
|
+
strategy: strategy.to_sym,
|
|
246
|
+
raw: true
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# Parse timeframe if provided
|
|
250
|
+
if timeframe
|
|
251
|
+
recall_opts[:timeframe] = parse_timeframe(timeframe)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
memories = htm.recall(query, **recall_opts)
|
|
255
|
+
|
|
256
|
+
results = memories.map do |memory|
|
|
257
|
+
node = HTM::Models::Node.includes(:tags).find(memory['id'])
|
|
258
|
+
{
|
|
259
|
+
id: memory['id'],
|
|
260
|
+
content: memory['content'],
|
|
261
|
+
tags: node.tags.map(&:name),
|
|
262
|
+
created_at: memory['created_at'],
|
|
263
|
+
score: memory['combined_score'] || memory['similarity']
|
|
264
|
+
}
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
MCP_STDERR_LOG.info "Recall complete: found #{results.length} memories"
|
|
268
|
+
|
|
269
|
+
{
|
|
270
|
+
success: true,
|
|
271
|
+
query: query,
|
|
272
|
+
strategy: strategy,
|
|
273
|
+
robot_name: htm.robot_name,
|
|
274
|
+
count: results.length,
|
|
275
|
+
results: results
|
|
276
|
+
}.to_json
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
private
|
|
280
|
+
|
|
281
|
+
def parse_timeframe(timeframe)
|
|
282
|
+
case timeframe.downcase
|
|
283
|
+
when 'today'
|
|
284
|
+
Time.now.beginning_of_day..Time.now
|
|
285
|
+
when 'this week'
|
|
286
|
+
1.week.ago..Time.now
|
|
287
|
+
when 'this month'
|
|
288
|
+
1.month.ago..Time.now
|
|
289
|
+
else
|
|
290
|
+
# Try to parse as ISO8601 range (start..end)
|
|
291
|
+
if timeframe.include?('..')
|
|
292
|
+
parts = timeframe.split('..')
|
|
293
|
+
Time.parse(parts[0])..Time.parse(parts[1])
|
|
294
|
+
else
|
|
295
|
+
# Single date - from that date to now
|
|
296
|
+
Time.parse(timeframe)..Time.now
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
rescue ArgumentError
|
|
300
|
+
nil # Invalid timeframe, skip filtering
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Tool: Forget a memory
|
|
305
|
+
class ForgetTool < FastMcp::Tool
|
|
306
|
+
description "Soft-delete a memory from HTM (can be restored later)"
|
|
307
|
+
|
|
308
|
+
arguments do
|
|
309
|
+
required(:node_id).filled(:integer).description("The ID of the node to forget")
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def call(node_id:)
|
|
313
|
+
htm = MCPSession.htm_instance
|
|
314
|
+
MCP_STDERR_LOG.info "ForgetTool called: node_id=#{node_id}, robot=#{htm.robot_name}"
|
|
315
|
+
|
|
316
|
+
htm.forget(node_id)
|
|
317
|
+
|
|
318
|
+
MCP_STDERR_LOG.info "Memory soft-deleted: node_id=#{node_id}"
|
|
319
|
+
|
|
320
|
+
{
|
|
321
|
+
success: true,
|
|
322
|
+
node_id: node_id,
|
|
323
|
+
robot_name: htm.robot_name,
|
|
324
|
+
message: "Memory soft-deleted. Use restore to recover."
|
|
325
|
+
}.to_json
|
|
326
|
+
rescue ActiveRecord::RecordNotFound
|
|
327
|
+
MCP_STDERR_LOG.warn "ForgetTool failed: node #{node_id} not found"
|
|
328
|
+
{
|
|
329
|
+
success: false,
|
|
330
|
+
error: "Node #{node_id} not found"
|
|
331
|
+
}.to_json
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Tool: Restore a forgotten memory
|
|
336
|
+
class RestoreTool < FastMcp::Tool
|
|
337
|
+
description "Restore a soft-deleted memory"
|
|
338
|
+
|
|
339
|
+
arguments do
|
|
340
|
+
required(:node_id).filled(:integer).description("The ID of the node to restore")
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def call(node_id:)
|
|
344
|
+
htm = MCPSession.htm_instance
|
|
345
|
+
MCP_STDERR_LOG.info "RestoreTool called: node_id=#{node_id}, robot=#{htm.robot_name}"
|
|
346
|
+
|
|
347
|
+
htm.restore(node_id)
|
|
348
|
+
|
|
349
|
+
MCP_STDERR_LOG.info "Memory restored: node_id=#{node_id}"
|
|
350
|
+
|
|
351
|
+
{
|
|
352
|
+
success: true,
|
|
353
|
+
node_id: node_id,
|
|
354
|
+
robot_name: htm.robot_name,
|
|
355
|
+
message: "Memory restored successfully"
|
|
356
|
+
}.to_json
|
|
357
|
+
rescue ActiveRecord::RecordNotFound
|
|
358
|
+
MCP_STDERR_LOG.warn "RestoreTool failed: node #{node_id} not found"
|
|
359
|
+
{
|
|
360
|
+
success: false,
|
|
361
|
+
error: "Node #{node_id} not found"
|
|
362
|
+
}.to_json
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Tool: List tags
|
|
367
|
+
class ListTagsTool < FastMcp::Tool
|
|
368
|
+
description "List all tags in HTM, optionally filtered by prefix"
|
|
369
|
+
|
|
370
|
+
arguments do
|
|
371
|
+
optional(:prefix).filled(:string).description("Filter tags by prefix (e.g., 'database' returns 'database:postgresql', etc.)")
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def call(prefix: nil)
|
|
375
|
+
MCP_STDERR_LOG.info "ListTagsTool called: prefix=#{prefix.inspect}"
|
|
376
|
+
|
|
377
|
+
tags_query = HTM::Models::Tag.order(:name)
|
|
378
|
+
tags_query = tags_query.where("name LIKE ?", "#{prefix}%") if prefix
|
|
379
|
+
|
|
380
|
+
tags = tags_query.map do |tag|
|
|
381
|
+
{
|
|
382
|
+
name: tag.name,
|
|
383
|
+
node_count: tag.nodes.count
|
|
384
|
+
}
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
MCP_STDERR_LOG.info "ListTagsTool complete: found #{tags.length} tags"
|
|
388
|
+
|
|
389
|
+
{
|
|
390
|
+
success: true,
|
|
391
|
+
prefix: prefix,
|
|
392
|
+
count: tags.length,
|
|
393
|
+
tags: tags
|
|
394
|
+
}.to_json
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Tool: Get memory statistics
|
|
399
|
+
class StatsTool < FastMcp::Tool
|
|
400
|
+
description "Get statistics about HTM memory usage"
|
|
401
|
+
|
|
402
|
+
arguments do
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def call
|
|
406
|
+
htm = MCPSession.htm_instance
|
|
407
|
+
robot = HTM::Models::Robot.find(htm.robot_id)
|
|
408
|
+
MCP_STDERR_LOG.info "StatsTool called for robot=#{htm.robot_name}"
|
|
409
|
+
|
|
410
|
+
# Note: Node uses default_scope to exclude deleted, so .count returns active nodes
|
|
411
|
+
total_nodes = HTM::Models::Node.count
|
|
412
|
+
deleted_nodes = HTM::Models::Node.deleted.count
|
|
413
|
+
nodes_with_embeddings = HTM::Models::Node.with_embeddings.count
|
|
414
|
+
nodes_with_tags = HTM::Models::Node.joins(:tags).distinct.count
|
|
415
|
+
total_tags = HTM::Models::Tag.count
|
|
416
|
+
total_robots = HTM::Models::Robot.count
|
|
417
|
+
|
|
418
|
+
MCP_STDERR_LOG.info "StatsTool complete: #{total_nodes} active nodes, #{total_tags} tags"
|
|
419
|
+
|
|
420
|
+
{
|
|
421
|
+
success: true,
|
|
422
|
+
current_robot: {
|
|
423
|
+
name: htm.robot_name,
|
|
424
|
+
id: htm.robot_id,
|
|
425
|
+
memory_summary: robot.memory_summary
|
|
426
|
+
},
|
|
427
|
+
statistics: {
|
|
428
|
+
nodes: {
|
|
429
|
+
active: total_nodes,
|
|
430
|
+
deleted: deleted_nodes,
|
|
431
|
+
with_embeddings: nodes_with_embeddings,
|
|
432
|
+
with_tags: nodes_with_tags
|
|
433
|
+
},
|
|
434
|
+
tags: {
|
|
435
|
+
total: total_tags
|
|
436
|
+
},
|
|
437
|
+
robots: {
|
|
438
|
+
total: total_robots
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}.to_json
|
|
442
|
+
rescue StandardError => e
|
|
443
|
+
MCP_STDERR_LOG.error "StatsTool error: #{e.message}"
|
|
444
|
+
{ success: false, error: e.message }.to_json
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Resource: Memory Statistics
|
|
449
|
+
class MemoryStatsResource < FastMcp::Resource
|
|
450
|
+
uri "htm://statistics"
|
|
451
|
+
resource_name "HTM Memory Statistics"
|
|
452
|
+
mime_type "application/json"
|
|
453
|
+
|
|
454
|
+
def content
|
|
455
|
+
htm = MCPSession.htm_instance
|
|
456
|
+
{
|
|
457
|
+
total_nodes: HTM::Models::Node.active.count,
|
|
458
|
+
total_tags: HTM::Models::Tag.count,
|
|
459
|
+
total_robots: HTM::Models::Robot.count,
|
|
460
|
+
current_robot: htm.robot_name,
|
|
461
|
+
robot_id: htm.robot_id,
|
|
462
|
+
robot_initialized: MCPSession.robot_initialized?,
|
|
463
|
+
embedding_provider: HTM.configuration.embedding_provider,
|
|
464
|
+
embedding_model: HTM.configuration.embedding_model
|
|
465
|
+
}.to_json
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Resource: Tag Hierarchy
|
|
470
|
+
class TagHierarchyResource < FastMcp::Resource
|
|
471
|
+
uri "htm://tags/hierarchy"
|
|
472
|
+
resource_name "HTM Tag Hierarchy"
|
|
473
|
+
mime_type "text/plain"
|
|
474
|
+
|
|
475
|
+
def content
|
|
476
|
+
HTM::Models::Tag.all.tree_string
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Resource: Recent Memories
|
|
481
|
+
class RecentMemoriesResource < FastMcp::Resource
|
|
482
|
+
uri "htm://memories/recent"
|
|
483
|
+
resource_name "Recent HTM Memories"
|
|
484
|
+
mime_type "application/json"
|
|
485
|
+
|
|
486
|
+
def content
|
|
487
|
+
recent = HTM::Models::Node.active
|
|
488
|
+
.includes(:tags)
|
|
489
|
+
.order(created_at: :desc)
|
|
490
|
+
.limit(20)
|
|
491
|
+
.map do |node|
|
|
492
|
+
{
|
|
493
|
+
id: node.id,
|
|
494
|
+
content: node.content[0..200],
|
|
495
|
+
tags: node.tags.map(&:name),
|
|
496
|
+
created_at: node.created_at.iso8601
|
|
497
|
+
}
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
{ recent_memories: recent }.to_json
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Create and configure the MCP server
|
|
505
|
+
server = FastMcp::Server.new(
|
|
506
|
+
name: 'htm-memory-server',
|
|
507
|
+
version: HTM::VERSION
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Register tools
|
|
511
|
+
server.register_tool(SetRobotTool) # Call first to set robot identity
|
|
512
|
+
server.register_tool(GetRobotTool) # Get current robot info
|
|
513
|
+
server.register_tool(GetWorkingMemoryTool) # Get working memory for session restore
|
|
514
|
+
server.register_tool(RememberTool)
|
|
515
|
+
server.register_tool(RecallTool)
|
|
516
|
+
server.register_tool(ForgetTool)
|
|
517
|
+
server.register_tool(RestoreTool)
|
|
518
|
+
server.register_tool(ListTagsTool)
|
|
519
|
+
server.register_tool(StatsTool)
|
|
520
|
+
|
|
521
|
+
# Register resources
|
|
522
|
+
server.register_resource(MemoryStatsResource)
|
|
523
|
+
server.register_resource(TagHierarchyResource)
|
|
524
|
+
server.register_resource(RecentMemoriesResource)
|
|
525
|
+
|
|
526
|
+
# Start the server (STDIO transport by default)
|
|
527
|
+
server.start
|