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.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/.aigcm_msg +1 -0
  3. data/.architecture/reviews/comprehensive-codebase-review.md +577 -0
  4. data/.claude/settings.local.json +95 -0
  5. data/.irbrc +283 -80
  6. data/.tbls.yml +2 -1
  7. data/CHANGELOG.md +327 -26
  8. data/CLAUDE.md +603 -0
  9. data/README.md +83 -12
  10. data/Rakefile +5 -0
  11. data/bin/htm_mcp.rb +527 -0
  12. data/db/migrate/{20250101000001_enable_extensions.rb → 00001_enable_extensions.rb} +0 -1
  13. data/db/migrate/00002_create_robots.rb +11 -0
  14. data/db/migrate/00003_create_file_sources.rb +20 -0
  15. data/db/migrate/00004_create_nodes.rb +65 -0
  16. data/db/migrate/00005_create_tags.rb +13 -0
  17. data/db/migrate/00006_create_node_tags.rb +18 -0
  18. data/db/migrate/00007_create_robot_nodes.rb +26 -0
  19. data/db/migrate/00009_add_working_memory_to_robot_nodes.rb +12 -0
  20. data/db/schema.sql +172 -1
  21. data/docs/api/database.md +1 -2
  22. data/docs/api/htm.md +197 -2
  23. data/docs/api/yard/HTM/ActiveRecordConfig.md +23 -0
  24. data/docs/api/yard/HTM/AuthorizationError.md +11 -0
  25. data/docs/api/yard/HTM/CircuitBreaker.md +92 -0
  26. data/docs/api/yard/HTM/CircuitBreakerOpenError.md +34 -0
  27. data/docs/api/yard/HTM/Configuration.md +175 -0
  28. data/docs/api/yard/HTM/Database.md +99 -0
  29. data/docs/api/yard/HTM/DatabaseError.md +14 -0
  30. data/docs/api/yard/HTM/EmbeddingError.md +18 -0
  31. data/docs/api/yard/HTM/EmbeddingService.md +58 -0
  32. data/docs/api/yard/HTM/Error.md +11 -0
  33. data/docs/api/yard/HTM/JobAdapter.md +39 -0
  34. data/docs/api/yard/HTM/LongTermMemory.md +342 -0
  35. data/docs/api/yard/HTM/NotFoundError.md +17 -0
  36. data/docs/api/yard/HTM/Observability.md +107 -0
  37. data/docs/api/yard/HTM/QueryTimeoutError.md +19 -0
  38. data/docs/api/yard/HTM/Railtie.md +27 -0
  39. data/docs/api/yard/HTM/ResourceExhaustedError.md +13 -0
  40. data/docs/api/yard/HTM/TagError.md +18 -0
  41. data/docs/api/yard/HTM/TagService.md +67 -0
  42. data/docs/api/yard/HTM/Timeframe/Result.md +24 -0
  43. data/docs/api/yard/HTM/Timeframe.md +40 -0
  44. data/docs/api/yard/HTM/TimeframeExtractor/Result.md +24 -0
  45. data/docs/api/yard/HTM/TimeframeExtractor.md +45 -0
  46. data/docs/api/yard/HTM/ValidationError.md +20 -0
  47. data/docs/api/yard/HTM/WorkingMemory.md +131 -0
  48. data/docs/api/yard/HTM.md +80 -0
  49. data/docs/api/yard/index.csv +179 -0
  50. data/docs/api/yard-reference.md +51 -0
  51. data/docs/database/README.md +128 -128
  52. data/docs/database/public.file_sources.md +42 -0
  53. data/docs/database/public.file_sources.svg +211 -0
  54. data/docs/database/public.node_tags.md +4 -4
  55. data/docs/database/public.node_tags.svg +212 -79
  56. data/docs/database/public.nodes.md +22 -12
  57. data/docs/database/public.nodes.svg +246 -127
  58. data/docs/database/public.robot_nodes.md +11 -9
  59. data/docs/database/public.robot_nodes.svg +220 -98
  60. data/docs/database/public.robots.md +2 -2
  61. data/docs/database/public.robots.svg +136 -81
  62. data/docs/database/public.tags.md +3 -3
  63. data/docs/database/public.tags.svg +118 -39
  64. data/docs/database/schema.json +850 -771
  65. data/docs/database/schema.svg +256 -197
  66. data/docs/development/schema.md +67 -2
  67. data/docs/guides/adding-memories.md +93 -7
  68. data/docs/guides/recalling-memories.md +36 -1
  69. data/examples/README.md +405 -0
  70. data/examples/cli_app/htm_cli.rb +65 -5
  71. data/examples/cli_app/temp.log +93 -0
  72. data/examples/file_loader_usage.rb +177 -0
  73. data/examples/mcp_client.rb +529 -0
  74. data/examples/robot_groups/lib/robot_group.rb +419 -0
  75. data/examples/robot_groups/lib/working_memory_channel.rb +140 -0
  76. data/examples/robot_groups/multi_process.rb +286 -0
  77. data/examples/robot_groups/robot_worker.rb +136 -0
  78. data/examples/robot_groups/same_process.rb +229 -0
  79. data/examples/timeframe_demo.rb +276 -0
  80. data/lib/htm/active_record_config.rb +1 -1
  81. data/lib/htm/circuit_breaker.rb +202 -0
  82. data/lib/htm/configuration.rb +59 -13
  83. data/lib/htm/database.rb +67 -36
  84. data/lib/htm/embedding_service.rb +39 -2
  85. data/lib/htm/errors.rb +131 -11
  86. data/lib/htm/jobs/generate_embedding_job.rb +5 -4
  87. data/lib/htm/jobs/generate_tags_job.rb +4 -0
  88. data/lib/htm/loaders/markdown_loader.rb +263 -0
  89. data/lib/htm/loaders/paragraph_chunker.rb +112 -0
  90. data/lib/htm/long_term_memory.rb +460 -343
  91. data/lib/htm/models/file_source.rb +99 -0
  92. data/lib/htm/models/node.rb +80 -5
  93. data/lib/htm/models/robot.rb +24 -1
  94. data/lib/htm/models/robot_node.rb +1 -0
  95. data/lib/htm/models/tag.rb +254 -4
  96. data/lib/htm/observability.rb +395 -0
  97. data/lib/htm/tag_service.rb +60 -3
  98. data/lib/htm/tasks.rb +26 -1
  99. data/lib/htm/timeframe.rb +194 -0
  100. data/lib/htm/timeframe_extractor.rb +307 -0
  101. data/lib/htm/version.rb +1 -1
  102. data/lib/htm/working_memory.rb +165 -70
  103. data/lib/htm.rb +328 -130
  104. data/lib/tasks/doc.rake +300 -0
  105. data/lib/tasks/files.rake +299 -0
  106. data/lib/tasks/htm.rake +158 -3
  107. data/lib/tasks/jobs.rake +3 -9
  108. data/lib/tasks/tags.rake +166 -6
  109. data/mkdocs.yml +36 -1
  110. data/notes/ARCHITECTURE_REVIEW.md +1167 -0
  111. data/notes/IMPLEMENTATION_SUMMARY.md +606 -0
  112. data/notes/MULTI_FRAMEWORK_IMPLEMENTATION.md +451 -0
  113. data/notes/next_steps.md +100 -0
  114. data/notes/plan.md +627 -0
  115. data/notes/tag_ontology_enhancement_ideas.md +222 -0
  116. data/notes/timescaledb_removal_summary.md +200 -0
  117. metadata +158 -17
  118. data/db/migrate/20250101000002_create_robots.rb +0 -14
  119. data/db/migrate/20250101000003_create_nodes.rb +0 -42
  120. data/db/migrate/20250101000005_create_tags.rb +0 -38
  121. data/db/migrate/20250101000007_add_node_vector_indexes.rb +0 -30
  122. data/db/migrate/20250125000001_add_content_hash_to_nodes.rb +0 -14
  123. data/db/migrate/20250125000002_create_robot_nodes.rb +0 -35
  124. data/db/migrate/20250125000003_remove_source_and_robot_id_from_nodes.rb +0 -28
  125. data/db/migrate/20250126000001_create_working_memories.rb +0 -19
  126. data/db/migrate/20250126000002_remove_unused_columns.rb +0 -12
  127. data/docs/database/public.working_memories.md +0 -40
  128. data/docs/database/public.working_memories.svg +0 -112
  129. data/lib/htm/models/working_memory_entry.rb +0 -88
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Multi-Process Robot Group Demo
5
+ #
6
+ # Demonstrates real-time synchronization across SEPARATE PROCESSES
7
+ # using PostgreSQL LISTEN/NOTIFY. Spawns robot_worker.rb as child processes.
8
+ #
9
+ # Key concepts:
10
+ # 1. Cross-process sync via PostgreSQL LISTEN/NOTIFY
11
+ # 2. Each robot runs as an independent process
12
+ # 3. Failover when a process dies
13
+ # 4. Dynamic scaling by spawning new processes
14
+ #
15
+ # Prerequisites:
16
+ # 1. Set HTM_DBURL environment variable
17
+ # 2. Initialize database schema: rake db_setup
18
+
19
+ require_relative '../../lib/htm'
20
+ require 'json'
21
+ require 'timeout'
22
+ require 'open3'
23
+
24
+ # =============================================================================
25
+ # Robot Process Manager
26
+ # =============================================================================
27
+
28
+ class RobotProcess
29
+ WORKER_SCRIPT = File.expand_path('robot_worker.rb', __dir__)
30
+
31
+ attr_reader :name, :pid
32
+
33
+ def initialize(name, group_name)
34
+ @name = name
35
+ @group_name = group_name
36
+
37
+ # Spawn the worker process
38
+ @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(
39
+ { 'HTM_DBURL' => ENV['HTM_DBURL'] },
40
+ 'ruby', WORKER_SCRIPT, name, group_name
41
+ )
42
+ @pid = @wait_thread.pid
43
+
44
+ # Start thread to read stderr (logs)
45
+ @log_thread = Thread.new do
46
+ @stderr.each_line { |line| print line }
47
+ rescue IOError
48
+ # Pipe closed
49
+ end
50
+ end
51
+
52
+ def send_command(cmd, **params)
53
+ command = { cmd: cmd }.merge(params)
54
+ @stdin.puts(command.to_json)
55
+ @stdin.flush
56
+
57
+ # Read response, skipping non-JSON lines
58
+ Timeout.timeout(10) do
59
+ loop do
60
+ line = @stdout.gets
61
+ return nil unless line
62
+
63
+ line = line.strip
64
+ next if line.empty?
65
+ next unless line.start_with?('{')
66
+
67
+ return JSON.parse(line, symbolize_names: true)
68
+ end
69
+ end
70
+ rescue Timeout::Error
71
+ { status: 'error', message: 'timeout' }
72
+ rescue Errno::EPIPE, IOError
73
+ { status: 'error', message: 'pipe closed' }
74
+ rescue JSON::ParserError => e
75
+ { status: 'error', message: "JSON parse error: #{e.message}" }
76
+ end
77
+
78
+ def shutdown
79
+ send_command('shutdown')
80
+ sleep 0.2
81
+ cleanup_pipes
82
+ @wait_thread.value
83
+ rescue Errno::EPIPE, IOError
84
+ # Already closed
85
+ end
86
+
87
+ def kill!
88
+ Process.kill('TERM', @pid)
89
+ cleanup_pipes
90
+ Process.wait(@pid)
91
+ rescue Errno::ESRCH, Errno::ECHILD
92
+ # Already dead
93
+ end
94
+
95
+ def alive?
96
+ @wait_thread && !@wait_thread.stop?
97
+ end
98
+
99
+ private
100
+
101
+ def cleanup_pipes
102
+ @stdin.close rescue nil
103
+ @stdout.close rescue nil
104
+ @stderr.close rescue nil
105
+ @log_thread.kill rescue nil
106
+ end
107
+ end
108
+
109
+ # =============================================================================
110
+ # Demo
111
+ # =============================================================================
112
+
113
+ def run_demo
114
+ puts <<~BANNER
115
+ ╔══════════════════════════════════════════════════════════════╗
116
+ ║ HTM Multi-Process Robot Group Demo ║
117
+ ║ Real-time Sync via PostgreSQL LISTEN/NOTIFY ║
118
+ ╚══════════════════════════════════════════════════════════════╝
119
+
120
+ BANNER
121
+
122
+ unless ENV['HTM_DBURL']
123
+ puts 'ERROR: HTM_DBURL not set.'
124
+ puts ' export HTM_DBURL="postgresql://user@localhost:5432/htm_development"'
125
+ exit 1
126
+ end
127
+
128
+ group_name = "demo-#{Time.now.to_i}"
129
+ robots = []
130
+
131
+ begin
132
+ # =========================================================================
133
+ # Scenario 1: Start robot processes
134
+ # =========================================================================
135
+ puts '━' * 60
136
+ puts "SCENARIO 1: Starting Robot Processes"
137
+ puts '━' * 60
138
+ puts
139
+
140
+ %w[robot-alpha robot-beta robot-gamma].each do |name|
141
+ robots << RobotProcess.new(name, group_name)
142
+ end
143
+
144
+ sleep 1.5
145
+
146
+ robots.each do |robot|
147
+ result = robot.send_command('ping')
148
+ status = result&.dig(:status) == 'ok' ? '✓' : '✗'
149
+ puts " #{status} #{robot.name} (PID #{robot.pid})"
150
+ end
151
+ puts
152
+
153
+ # =========================================================================
154
+ # Scenario 2: Cross-process memory sharing
155
+ # =========================================================================
156
+ puts '━' * 60
157
+ puts "SCENARIO 2: Cross-Process Memory Sharing"
158
+ puts '━' * 60
159
+ puts
160
+ puts " Alpha adds memories, others receive notifications..."
161
+ puts
162
+
163
+ alpha, beta, gamma = robots
164
+
165
+ alpha.send_command('remember', content: 'Customer John Smith prefers morning appointments.')
166
+ sleep 0.5
167
+ alpha.send_command('remember', content: 'Account #A-789 has a pending refund for $150.')
168
+ sleep 0.5
169
+ alpha.send_command('remember', content: 'Escalate billing issues to finance team.')
170
+ sleep 1.0
171
+
172
+ puts
173
+ puts " Working memory status:"
174
+ robots.each do |robot|
175
+ status = robot.send_command('status')
176
+ next unless status&.dig(:status) == 'ok'
177
+
178
+ puts " #{robot.name}: #{status[:working_memory_nodes]} nodes, #{status[:notifications_received]} notifications"
179
+ end
180
+ puts
181
+
182
+ # =========================================================================
183
+ # Scenario 3: Collaborative memory
184
+ # =========================================================================
185
+ puts '━' * 60
186
+ puts "SCENARIO 3: Collaborative Memory"
187
+ puts '━' * 60
188
+ puts
189
+
190
+ beta.send_command('remember', content: 'Customer confirmed refund was processed.')
191
+ sleep 0.5
192
+ gamma.send_command('remember', content: 'Customer wants email confirmation.')
193
+ sleep 1.0
194
+
195
+ puts " Each robot recalls 'refund':"
196
+ robots.each do |robot|
197
+ result = robot.send_command('recall', query: 'refund', limit: 3)
198
+ next unless result&.dig(:status) == 'ok'
199
+
200
+ puts " #{robot.name}: found #{result[:count]} memories"
201
+ end
202
+ puts
203
+
204
+ # =========================================================================
205
+ # Scenario 4: Failover
206
+ # =========================================================================
207
+ puts '━' * 60
208
+ puts "SCENARIO 4: Simulated Failover"
209
+ puts '━' * 60
210
+ puts
211
+
212
+ puts " Killing robot-alpha..."
213
+ alpha.kill!
214
+ robots.delete(alpha)
215
+ sleep 0.5
216
+ puts " ⚠ robot-alpha terminated"
217
+ puts
218
+
219
+ puts " Remaining robots retain context:"
220
+ robots.each do |robot|
221
+ status = robot.send_command('status')
222
+ result = robot.send_command('recall', query: 'customer', limit: 5)
223
+ next unless status && result
224
+
225
+ puts " #{robot.name}: #{status[:working_memory_nodes]} nodes, recalls #{result[:count]}"
226
+ end
227
+ puts
228
+ puts " ✓ Failover successful"
229
+ puts
230
+
231
+ # =========================================================================
232
+ # Scenario 5: Dynamic scaling
233
+ # =========================================================================
234
+ puts '━' * 60
235
+ puts "SCENARIO 5: Dynamic Scaling"
236
+ puts '━' * 60
237
+ puts
238
+
239
+ puts " Adding robot-delta..."
240
+ delta = RobotProcess.new('robot-delta', group_name)
241
+ robots << delta
242
+ sleep 1.5
243
+
244
+ result = delta.send_command('ping')
245
+ puts " ✓ robot-delta (PID #{delta.pid}) joined" if result&.dig(:status) == 'ok'
246
+
247
+ delta.send_command('remember', content: 'New robot ready to assist.')
248
+ sleep 1.0
249
+
250
+ puts
251
+ puts " Notifications received:"
252
+ robots.each do |robot|
253
+ status = robot.send_command('status')
254
+ next unless status&.dig(:status) == 'ok'
255
+
256
+ puts " #{robot.name}: #{status[:notifications_received]}"
257
+ end
258
+ puts
259
+
260
+ # =========================================================================
261
+ # Summary
262
+ # =========================================================================
263
+ puts '━' * 60
264
+ puts "DEMO COMPLETE"
265
+ puts '━' * 60
266
+ puts
267
+ puts " Demonstrated:"
268
+ puts " • Real-time sync across #{robots.length} processes"
269
+ puts " • PostgreSQL LISTEN/NOTIFY pub/sub"
270
+ puts " • Failover with context preservation"
271
+ puts " • Dynamic scaling"
272
+ puts
273
+
274
+ ensure
275
+ puts "Cleaning up..."
276
+ robots.each do |robot|
277
+ next unless robot.alive?
278
+
279
+ robot.shutdown
280
+ puts " Stopped #{robot.name}"
281
+ end
282
+ puts "Done."
283
+ end
284
+ end
285
+
286
+ run_demo if __FILE__ == $PROGRAM_NAME
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Robot Worker - Standalone process that participates in a robot group
5
+ #
6
+ # Usage: ruby robot_worker.rb <robot_name> <group_name>
7
+ #
8
+ # Communication:
9
+ # - Receives JSON commands via stdin
10
+ # - Sends JSON responses via stdout
11
+ # - Logs to stderr
12
+ #
13
+ # Commands:
14
+ # { "cmd": "ping" }
15
+ # { "cmd": "remember", "content": "..." }
16
+ # { "cmd": "recall", "query": "...", "limit": 5 }
17
+ # { "cmd": "status" }
18
+ # { "cmd": "shutdown" }
19
+
20
+ require 'logger'
21
+ require 'json'
22
+ require_relative '../../lib/htm'
23
+ require_relative 'lib/working_memory_channel'
24
+
25
+ robot_name = ARGV[0]
26
+ group_name = ARGV[1]
27
+
28
+ unless robot_name && group_name
29
+ $stderr.puts "Usage: ruby robot_worker.rb <robot_name> <group_name>"
30
+ exit 1
31
+ end
32
+
33
+ def log(robot_name, message)
34
+ timestamp = Time.now.strftime('%H:%M:%S.%L')
35
+ $stderr.puts "[#{timestamp}] [#{robot_name}] #{message}"
36
+ $stderr.flush
37
+ end
38
+
39
+ # Configure HTM with logger to stderr (keep stdout clean for JSON)
40
+ HTM.configure do |config|
41
+ config.embedding_provider = :ollama
42
+ config.embedding_model = 'nomic-embed-text:latest'
43
+ config.embedding_dimensions = 768
44
+ config.tag_provider = :ollama
45
+ config.tag_model = 'gemma3:latest'
46
+ config.logger = Logger.new($stderr, level: Logger::WARN)
47
+ end
48
+
49
+ log(robot_name, 'Starting up...')
50
+
51
+ # Create HTM instance for this robot
52
+ htm = HTM.new(robot_name: robot_name, working_memory_size: 8000)
53
+ db_config = HTM::Database.default_config
54
+
55
+ # Setup channel for cross-process notifications
56
+ channel = WorkingMemoryChannel.new(group_name, db_config)
57
+
58
+ # Track notifications received
59
+ notifications_count = 0
60
+ channel.on_change do |event, node_id, origin_robot_id|
61
+ next if origin_robot_id == htm.robot_id
62
+
63
+ notifications_count += 1
64
+ log(robot_name, "Received #{event} for node #{node_id}")
65
+
66
+ case event
67
+ when :added
68
+ node = HTM::Models::Node.find_by(id: node_id)
69
+ if node
70
+ htm.working_memory.add_from_sync(
71
+ id: node.id,
72
+ content: node.content,
73
+ token_count: node.token_count || 0,
74
+ created_at: node.created_at
75
+ )
76
+ end
77
+ when :evicted
78
+ htm.working_memory.remove_from_sync(node_id)
79
+ when :cleared
80
+ htm.working_memory.clear_from_sync
81
+ end
82
+ end
83
+
84
+ channel.start_listening
85
+ log(robot_name, "Listening on channel: #{channel.channel_name}")
86
+
87
+ # Process commands from stdin
88
+ $stdin.each_line do |line|
89
+ begin
90
+ command = JSON.parse(line.strip, symbolize_names: true)
91
+
92
+ result = case command[:cmd]
93
+ when 'remember'
94
+ log(robot_name, "Remembering: #{command[:content][0..40]}...")
95
+ node_id = htm.remember(command[:content])
96
+ channel.notify(:added, node_id: node_id, robot_id: htm.robot_id)
97
+ log(robot_name, "Sent notification for node #{node_id}")
98
+ { status: 'ok', node_id: node_id }
99
+
100
+ when 'recall'
101
+ log(robot_name, "Recalling: #{command[:query]}")
102
+ results = htm.recall(command[:query], limit: command[:limit] || 5, strategy: :fulltext, raw: true)
103
+ { status: 'ok', count: results.length }
104
+
105
+ when 'status'
106
+ {
107
+ status: 'ok',
108
+ robot_id: htm.robot_id,
109
+ working_memory_nodes: htm.working_memory.node_count,
110
+ working_memory_tokens: htm.working_memory.token_count,
111
+ notifications_received: notifications_count
112
+ }
113
+
114
+ when 'ping'
115
+ { status: 'ok', message: 'pong', robot: robot_name }
116
+
117
+ when 'shutdown'
118
+ channel.stop_listening
119
+ log(robot_name, 'Shutting down.')
120
+ puts({ status: 'ok', message: 'bye' }.to_json)
121
+ $stdout.flush
122
+ exit 0
123
+
124
+ else
125
+ { status: 'error', message: "Unknown command: #{command[:cmd]}" }
126
+ end
127
+
128
+ puts result.to_json
129
+ $stdout.flush
130
+ rescue StandardError => e
131
+ puts({ status: 'error', message: e.message }.to_json)
132
+ $stdout.flush
133
+ end
134
+ end
135
+
136
+ channel.stop_listening
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Robot Group Demo - Shared Working Memory & Failover
5
+ #
6
+ # This example demonstrates an application-level pattern for coordinating
7
+ # multiple robots with shared working memory. Key concepts:
8
+ #
9
+ # 1. **Shared Working Memory**: Multiple robots can share the same working
10
+ # memory by having working_memory=true for the same nodes in robot_nodes.
11
+ #
12
+ # 2. **Active/Passive Roles**: Active robots participate in conversations;
13
+ # passive robots maintain synchronized context for instant failover.
14
+ #
15
+ # 3. **Failover**: When an active robot fails, a passive robot can take over
16
+ # with full context already loaded (warm standby).
17
+ #
18
+ # 4. **Real-time Sync**: PostgreSQL LISTEN/NOTIFY enables real-time
19
+ # synchronization of in-memory working memory across robots.
20
+ #
21
+ # Prerequisites:
22
+ # 1. Set HTM_DBURL environment variable
23
+ # 2. Initialize database schema: rake db_setup
24
+ # 3. Install dependencies: bundle install
25
+
26
+ require_relative '../../lib/htm'
27
+ require 'json'
28
+
29
+ require_relative 'lib/working_memory_channel'
30
+ require_relative 'lib/robot_group'
31
+
32
+ # =============================================================================
33
+ # Demo Script
34
+ # =============================================================================
35
+
36
+ puts 'HTM Robot Group Demo - Shared Working Memory & Failover'
37
+ puts '=' * 60
38
+
39
+ unless ENV['HTM_DBURL']
40
+ puts 'ERROR: HTM_DBURL not set. Please set it:'
41
+ puts ' export HTM_DBURL="postgresql://postgres@localhost:5432/htm_development"'
42
+ exit 1
43
+ end
44
+
45
+ begin
46
+ # Configure HTM
47
+ puts "\n1. Configuring HTM..."
48
+ HTM.configure do |config|
49
+ config.embedding_provider = :ollama
50
+ config.embedding_model = 'nomic-embed-text:latest'
51
+ config.embedding_dimensions = 768
52
+ config.tag_provider = :ollama
53
+ config.tag_model = 'gemma3:latest'
54
+ end
55
+ puts '✓ HTM configured'
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Scenario 1: Create a high-availability robot group
59
+ # ---------------------------------------------------------------------------
60
+ puts "\n2. Creating robot group with primary + standby..."
61
+
62
+ group = RobotGroup.new(
63
+ name: 'customer-support-ha',
64
+ active: ['support-primary'],
65
+ passive: ['support-standby'],
66
+ max_tokens: 8000
67
+ )
68
+
69
+ status = group.status
70
+ puts "✓ Group created: #{status[:name]}"
71
+ puts " Active: #{status[:active].join(', ')}"
72
+ puts " Passive: #{status[:passive].join(', ')}"
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Scenario 2: Add shared memories
76
+ # ---------------------------------------------------------------------------
77
+ puts "\n3. Adding memories to shared working memory..."
78
+
79
+ group.remember(
80
+ 'Customer account #12345 prefers email communication over phone calls.',
81
+ originator: 'support-primary'
82
+ )
83
+ puts ' ✓ Remembered customer preference'
84
+
85
+ group.remember(
86
+ 'Open ticket #789: Customer reported billing discrepancy on invoice dated Nov 15.',
87
+ originator: 'support-primary'
88
+ )
89
+ puts ' ✓ Remembered open ticket'
90
+
91
+ group.remember(
92
+ 'Customer has been with us for 5 years and has premium tier subscription.',
93
+ originator: 'support-primary'
94
+ )
95
+ puts ' ✓ Remembered customer status'
96
+
97
+ # Brief pause for async jobs
98
+ sleep 0.3
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # Scenario 3: Verify synchronization
102
+ # ---------------------------------------------------------------------------
103
+ puts "\n4. Verifying working memory synchronization..."
104
+
105
+ status = group.status
106
+ puts " Working memory nodes: #{status[:working_memory_nodes]}"
107
+ puts " Token utilization: #{(status[:token_utilization] * 100).round(1)}%"
108
+ puts " In sync: #{status[:in_sync] ? '✓ Yes' : '✗ No'}"
109
+
110
+ # Force sync if needed
111
+ unless status[:in_sync]
112
+ result = group.sync_all
113
+ puts " Synced #{result[:synced_nodes]} nodes to #{result[:members_updated]} members"
114
+ end
115
+
116
+ # ---------------------------------------------------------------------------
117
+ # Scenario 4: Simulate primary robot failure and failover
118
+ # ---------------------------------------------------------------------------
119
+ puts "\n5. Simulating failover scenario..."
120
+ puts " ⚠ Primary robot 'support-primary' has stopped responding!"
121
+
122
+ # Failover to standby
123
+ group.failover!
124
+
125
+ status = group.status
126
+ puts " Active robots now: #{status[:active].join(', ')}"
127
+ puts " Passive robots now: #{status[:passive].join(', ') || '(none)'}"
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # Scenario 5: Verify standby has full context
131
+ # ---------------------------------------------------------------------------
132
+ puts "\n6. Verifying standby has full context after failover..."
133
+
134
+ # Use fulltext search (doesn't require embeddings)
135
+ memories = group.recall('customer', limit: 5, strategy: :fulltext, raw: true)
136
+ puts " ✓ Standby recalled #{memories.length} memories about 'customer'"
137
+ memories.each do |memory|
138
+ content = memory['content'] || memory[:content]
139
+ puts " - #{content[0..55]}..."
140
+ end
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Scenario 6: Add a new active robot (scaling up)
144
+ # ---------------------------------------------------------------------------
145
+ puts "\n7. Adding a second active robot (scaling up)..."
146
+
147
+ group.add_active('support-secondary')
148
+ group.sync_robot('support-secondary')
149
+
150
+ status = group.status
151
+ puts " ✓ Now running with #{status[:active].length} active robots"
152
+ puts " Active: #{status[:active].join(', ')}"
153
+ puts " In sync: #{status[:in_sync] ? '✓ Yes' : '✗ No'}"
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Scenario 7: Collaborative memory - both robots can add
157
+ # ---------------------------------------------------------------------------
158
+ puts "\n8. Demonstrating collaborative memory..."
159
+
160
+ # This memory is added through the group and synced to all
161
+ group.remember(
162
+ 'Customer called again - issue escalated to billing department.',
163
+ originator: 'support-secondary'
164
+ )
165
+ puts ' ✓ Secondary robot added memory, synced to all'
166
+
167
+ status = group.status
168
+ puts " Total shared memories: #{status[:working_memory_nodes]}"
169
+
170
+ # ---------------------------------------------------------------------------
171
+ # Scenario 8: Real-time sync via PostgreSQL LISTEN/NOTIFY
172
+ # ---------------------------------------------------------------------------
173
+ puts "\n9. Demonstrating real-time sync via PostgreSQL LISTEN/NOTIFY..."
174
+
175
+ # Check initial in-memory state of each robot
176
+ puts ' Initial in-memory working memory state:'
177
+ puts " support-standby: #{group.instance_variable_get(:@active_robots)['support-standby']&.working_memory&.node_count || 0} nodes"
178
+ puts " support-secondary: #{group.instance_variable_get(:@active_robots)['support-secondary']&.working_memory&.node_count || 0} nodes"
179
+
180
+ # Add a memory from secondary robot
181
+ puts "\n Adding memory from support-secondary..."
182
+ group.remember(
183
+ 'Resolution: Refund issued for $47.50 - billing error confirmed.',
184
+ originator: 'support-secondary'
185
+ )
186
+
187
+ # Give the LISTEN/NOTIFY a moment to propagate
188
+ sleep 0.2
189
+
190
+ # Check sync stats
191
+ sync_stats = group.sync_stats
192
+ puts "\n Real-time sync statistics:"
193
+ puts " Nodes synced via NOTIFY: #{sync_stats[:nodes_synced]}"
194
+ puts " Evictions synced: #{sync_stats[:evictions_synced]}"
195
+ puts " Channel notifications received: #{group.channel.notifications_received}"
196
+ puts " Listener active: #{group.channel.listening? ? '✓ Yes' : '✗ No'}"
197
+
198
+ # Verify both robots have the memory in their in-memory cache
199
+ puts "\n In-memory working memory after sync:"
200
+ group.instance_variable_get(:@active_robots).each do |name, htm|
201
+ puts " #{name}: #{htm.working_memory.node_count} nodes"
202
+ end
203
+
204
+ # ---------------------------------------------------------------------------
205
+ # Summary
206
+ # ---------------------------------------------------------------------------
207
+ puts "\n" + '=' * 60
208
+ puts 'Demo Complete!'
209
+ puts "\nRobotGroup enables:"
210
+ puts ' • Shared working memory across multiple robots'
211
+ puts ' • Instant failover with warm standby (passive robots)'
212
+ puts ' • Collaborative context building'
213
+ puts ' • Dynamic scaling (add/remove robots)'
214
+ puts ' • Real-time sync via PostgreSQL LISTEN/NOTIFY'
215
+ puts "\nFinal group status:"
216
+ status = group.status
217
+ status.each { |k, v| puts " #{k}: #{v}" }
218
+
219
+ # Cleanup
220
+ puts "\nCleaning up..."
221
+ group.clear_working_memory
222
+ group.shutdown
223
+ puts '✓ Cleared shared working memory and stopped listener'
224
+ rescue StandardError => e
225
+ puts "\n✗ Error: #{e.message}"
226
+ puts e.backtrace.first(5).join("\n")
227
+ group&.shutdown # Ensure we clean up on error too
228
+ exit 1
229
+ end