htm 0.0.14 → 0.0.15
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/CHANGELOG.md +33 -0
- data/README.md +269 -79
- data/db/migrate/00003_create_file_sources.rb +5 -0
- data/db/migrate/00004_create_nodes.rb +17 -0
- data/db/migrate/00005_create_tags.rb +7 -0
- data/db/migrate/00006_create_node_tags.rb +2 -0
- data/db/migrate/00007_create_robot_nodes.rb +7 -0
- data/db/schema.sql +41 -29
- data/docs/api/yard/HTM/Configuration.md +54 -0
- data/docs/api/yard/HTM/Database.md +13 -10
- data/docs/api/yard/HTM/EmbeddingService.md +5 -1
- data/docs/api/yard/HTM/LongTermMemory.md +18 -277
- data/docs/api/yard/HTM/PropositionError.md +18 -0
- data/docs/api/yard/HTM/PropositionService.md +66 -0
- data/docs/api/yard/HTM/QueryCache.md +88 -0
- data/docs/api/yard/HTM/RobotGroup.md +481 -0
- data/docs/api/yard/HTM/SqlBuilder.md +108 -0
- data/docs/api/yard/HTM/TagService.md +4 -0
- data/docs/api/yard/HTM/Telemetry/NullInstrument.md +13 -0
- data/docs/api/yard/HTM/Telemetry/NullMeter.md +15 -0
- data/docs/api/yard/HTM/Telemetry.md +109 -0
- data/docs/api/yard/HTM/WorkingMemoryChannel.md +176 -0
- data/docs/api/yard/HTM.md +11 -23
- data/docs/api/yard/index.csv +102 -25
- data/docs/api/yard-reference.md +8 -0
- data/docs/assets/images/multi-provider-failover.svg +51 -0
- data/docs/assets/images/robot-group-architecture.svg +65 -0
- data/docs/database/README.md +3 -3
- data/docs/database/public.file_sources.svg +29 -21
- data/docs/database/public.node_tags.md +2 -0
- data/docs/database/public.node_tags.svg +53 -41
- data/docs/database/public.nodes.md +2 -0
- data/docs/database/public.nodes.svg +52 -40
- data/docs/database/public.robot_nodes.md +2 -0
- data/docs/database/public.robot_nodes.svg +30 -22
- data/docs/database/public.robots.svg +16 -12
- data/docs/database/public.tags.md +3 -0
- data/docs/database/public.tags.svg +41 -33
- data/docs/database/schema.json +66 -0
- data/docs/database/schema.svg +60 -48
- data/docs/development/index.md +13 -0
- data/docs/development/rake-tasks.md +1068 -0
- data/docs/getting-started/quick-start.md +144 -155
- data/docs/guides/adding-memories.md +2 -3
- data/docs/guides/context-assembly.md +185 -184
- data/docs/guides/getting-started.md +154 -148
- data/docs/guides/index.md +7 -0
- data/docs/guides/long-term-memory.md +60 -92
- data/docs/guides/mcp-server.md +617 -0
- data/docs/guides/multi-robot.md +249 -345
- data/docs/guides/recalling-memories.md +153 -163
- data/docs/guides/robot-groups.md +604 -0
- data/docs/guides/search-strategies.md +61 -58
- data/docs/guides/working-memory.md +103 -136
- data/docs/index.md +30 -26
- data/examples/robot_groups/robot_worker.rb +1 -2
- data/examples/robot_groups/same_process.rb +1 -4
- data/lib/htm/robot_group.rb +721 -0
- data/lib/htm/version.rb +1 -1
- data/lib/htm/working_memory_channel.rb +250 -0
- data/lib/htm.rb +2 -0
- data/mkdocs.yml +2 -0
- metadata +18 -9
- data/db/migrate/00009_add_working_memory_to_robot_nodes.rb +0 -12
- data/db/migrate/00010_add_soft_delete_to_associations.rb +0 -29
- data/db/migrate/00011_add_performance_indexes.rb +0 -21
- data/db/migrate/00012_add_tags_trigram_index.rb +0 -18
- data/db/migrate/00013_enable_lz4_compression.rb +0 -43
- data/examples/robot_groups/lib/robot_group.rb +0 -419
- data/examples/robot_groups/lib/working_memory_channel.rb +0 -140
data/docs/guides/multi-robot.md
CHANGED
|
@@ -23,173 +23,174 @@ In HTM, all robots share the same long-term memory database but maintain separat
|
|
|
23
23
|
# Robot 1: Research Assistant
|
|
24
24
|
research_bot = HTM.new(
|
|
25
25
|
robot_name: "Research Assistant",
|
|
26
|
-
robot_id: "research-001",
|
|
27
26
|
working_memory_size: 128_000
|
|
28
27
|
)
|
|
28
|
+
# Each robot gets a unique robot_id automatically
|
|
29
29
|
|
|
30
30
|
# Robot 2: Code Helper
|
|
31
31
|
code_bot = HTM.new(
|
|
32
32
|
robot_name: "Code Helper",
|
|
33
|
-
robot_id: "code-001",
|
|
34
33
|
working_memory_size: 128_000
|
|
35
34
|
)
|
|
36
35
|
|
|
37
36
|
# Robot 3: Documentation Writer
|
|
38
37
|
docs_bot = HTM.new(
|
|
39
38
|
robot_name: "Docs Writer",
|
|
40
|
-
robot_id: "docs-001",
|
|
41
39
|
working_memory_size: 64_000
|
|
42
40
|
)
|
|
43
41
|
|
|
44
|
-
# Each robot can access shared knowledge
|
|
45
|
-
research_bot.
|
|
46
|
-
"finding_001",
|
|
42
|
+
# Each robot can access shared knowledge through the shared database
|
|
43
|
+
research_bot.remember(
|
|
47
44
|
"Research shows PostgreSQL outperforms MongoDB for ACID workloads",
|
|
48
|
-
|
|
49
|
-
importance: 8.0,
|
|
50
|
-
tags: ["research", "database"]
|
|
45
|
+
tags: ["research", "database:comparison"]
|
|
51
46
|
)
|
|
52
47
|
|
|
53
|
-
# Code bot can access
|
|
48
|
+
# Code bot can access research findings (shared long-term memory)
|
|
54
49
|
findings = code_bot.recall(
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
"database performance",
|
|
51
|
+
timeframe: "last hour"
|
|
57
52
|
)
|
|
58
53
|
|
|
59
|
-
# Docs bot can document
|
|
60
|
-
docs_bot.
|
|
61
|
-
"doc_001",
|
|
54
|
+
# Docs bot can document findings
|
|
55
|
+
docs_bot.remember(
|
|
62
56
|
"PostgreSQL performance documented based on research findings",
|
|
63
|
-
type: :context,
|
|
64
|
-
importance: 6.0,
|
|
65
57
|
tags: ["documentation", "database"],
|
|
66
|
-
|
|
58
|
+
metadata: { source: "research" }
|
|
67
59
|
)
|
|
68
60
|
```
|
|
69
61
|
|
|
70
62
|
## Robot Identification
|
|
71
63
|
|
|
72
|
-
###
|
|
64
|
+
### Naming Strategies
|
|
73
65
|
|
|
74
|
-
Choose
|
|
66
|
+
Choose a consistent naming strategy for robot identification:
|
|
75
67
|
|
|
76
68
|
```ruby
|
|
77
69
|
# Strategy 1: Persistent Robot (recommended for production)
|
|
70
|
+
# Use descriptive names - robot_id is assigned automatically
|
|
78
71
|
persistent_bot = HTM.new(
|
|
79
|
-
robot_name: "Production Assistant"
|
|
80
|
-
robot_id: "prod-assistant-001" # Fixed, reusable
|
|
72
|
+
robot_name: "Production Assistant"
|
|
81
73
|
)
|
|
74
|
+
puts "Robot ID: #{persistent_bot.robot_id}" # e.g., 1
|
|
75
|
+
puts "Robot Name: #{persistent_bot.robot_name}" # "Production Assistant"
|
|
82
76
|
|
|
83
77
|
# Strategy 2: Session-based Robot (for temporary workflows)
|
|
84
|
-
session_id = SecureRandom.uuid
|
|
78
|
+
session_id = SecureRandom.uuid[0..7]
|
|
85
79
|
session_bot = HTM.new(
|
|
86
|
-
robot_name: "
|
|
87
|
-
robot_id: "session-#{session_id}" # Unique per session
|
|
80
|
+
robot_name: "Session #{session_id}"
|
|
88
81
|
)
|
|
89
82
|
|
|
90
83
|
# Strategy 3: User-specific Robot
|
|
91
84
|
user_id = "alice"
|
|
92
85
|
user_bot = HTM.new(
|
|
93
|
-
robot_name: "
|
|
94
|
-
robot_id: "user-#{user_id}-assistant"
|
|
86
|
+
robot_name: "#{user_id}'s Assistant"
|
|
95
87
|
)
|
|
96
88
|
```
|
|
97
89
|
|
|
98
90
|
!!! tip "Naming Conventions"
|
|
99
|
-
|
|
100
|
-
- **
|
|
101
|
-
- **
|
|
102
|
-
- **
|
|
91
|
+
Use descriptive `robot_name` values to identify robots:
|
|
92
|
+
- **Production robots**: `"Production Assistant"`, `"API Helper"`
|
|
93
|
+
- **User robots**: `"Alice's Assistant"`, `"User 123 Bot"`
|
|
94
|
+
- **Session robots**: `"Session abc123"`, `"Temp Session"`
|
|
95
|
+
- **Team robots**: `"Engineering Reviewer"`, `"QA Bot"`
|
|
103
96
|
|
|
104
97
|
### Robot Registry
|
|
105
98
|
|
|
106
|
-
All robots are automatically registered:
|
|
99
|
+
All robots are automatically registered in the database:
|
|
107
100
|
|
|
108
101
|
```ruby
|
|
109
102
|
# Robots are registered when created
|
|
110
|
-
bot = HTM.new(robot_name: "My Bot"
|
|
111
|
-
|
|
112
|
-
# Query robot registry
|
|
113
|
-
config = HTM::Database.default_config
|
|
114
|
-
conn = PG.connect(config)
|
|
115
|
-
|
|
116
|
-
result = conn.exec("SELECT * FROM robots ORDER BY last_active DESC")
|
|
103
|
+
bot = HTM.new(robot_name: "My Bot")
|
|
104
|
+
puts "Registered with ID: #{bot.robot_id}"
|
|
117
105
|
|
|
106
|
+
# Query robot registry using ActiveRecord
|
|
118
107
|
puts "Registered robots:"
|
|
119
|
-
|
|
120
|
-
puts "#{
|
|
121
|
-
puts " Created: #{
|
|
122
|
-
puts " Last active: #{
|
|
108
|
+
HTM::Models::Robot.order(last_active_at: :desc).each do |robot|
|
|
109
|
+
puts "#{robot.name} (ID: #{robot.id})"
|
|
110
|
+
puts " Created: #{robot.created_at}"
|
|
111
|
+
puts " Last active: #{robot.last_active_at}"
|
|
123
112
|
puts
|
|
124
113
|
end
|
|
125
|
-
|
|
126
|
-
conn.close
|
|
127
114
|
```
|
|
128
115
|
|
|
129
116
|
## Attribution Tracking
|
|
130
117
|
|
|
131
118
|
### Who Said What?
|
|
132
119
|
|
|
133
|
-
Track which robot contributed which memories:
|
|
120
|
+
Track which robot contributed which memories using the robot_nodes join table:
|
|
134
121
|
|
|
135
122
|
```ruby
|
|
136
123
|
# Add memories from different robots
|
|
137
|
-
alpha = HTM.new(robot_name: "Alpha"
|
|
138
|
-
beta = HTM.new(robot_name: "Beta"
|
|
124
|
+
alpha = HTM.new(robot_name: "Alpha")
|
|
125
|
+
beta = HTM.new(robot_name: "Beta")
|
|
139
126
|
|
|
140
|
-
alpha.
|
|
141
|
-
beta.
|
|
127
|
+
alpha.remember("Alpha's insight about caching", tags: ["caching"])
|
|
128
|
+
beta.remember("Beta's approach to testing", tags: ["testing"])
|
|
142
129
|
|
|
143
|
-
# Query
|
|
130
|
+
# Query memories linked to a specific robot via RobotNode
|
|
144
131
|
def memories_by_robot(robot_id)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
result = conn.exec_params(
|
|
149
|
-
"SELECT key, value, type FROM nodes WHERE robot_id = $1",
|
|
150
|
-
[robot_id]
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
memories = result.to_a
|
|
154
|
-
conn.close
|
|
155
|
-
memories
|
|
132
|
+
HTM::Models::RobotNode.where(robot_id: robot_id)
|
|
133
|
+
.includes(:node)
|
|
134
|
+
.map { |rn| rn.node.attributes }
|
|
156
135
|
end
|
|
157
136
|
|
|
158
|
-
alpha_memories = memories_by_robot(
|
|
137
|
+
alpha_memories = memories_by_robot(alpha.robot_id)
|
|
159
138
|
puts "Alpha contributed #{alpha_memories.length} memories"
|
|
160
139
|
```
|
|
161
140
|
|
|
162
|
-
###
|
|
141
|
+
### Tracking Contributions
|
|
163
142
|
|
|
164
|
-
|
|
143
|
+
Query which robots have contributed to specific topics:
|
|
165
144
|
|
|
166
145
|
```ruby
|
|
167
|
-
# Find
|
|
168
|
-
|
|
146
|
+
# Find nodes matching a topic and see which robots contributed
|
|
147
|
+
def robots_discussing(topic)
|
|
148
|
+
# Get matching nodes
|
|
149
|
+
nodes = HTM::Models::Node.where("content ILIKE ?", "%#{topic}%")
|
|
150
|
+
|
|
151
|
+
# Count contributions by robot
|
|
152
|
+
robot_counts = Hash.new(0)
|
|
153
|
+
nodes.each do |node|
|
|
154
|
+
node.robot_nodes.each do |rn|
|
|
155
|
+
robot = HTM::Models::Robot.find(rn.robot_id)
|
|
156
|
+
robot_counts[robot.name] += 1
|
|
157
|
+
end
|
|
158
|
+
end
|
|
169
159
|
|
|
170
|
-
|
|
171
|
-
breakdown.each do |robot_id, count|
|
|
172
|
-
puts " #{robot_id}: #{count} mentions"
|
|
160
|
+
robot_counts
|
|
173
161
|
end
|
|
174
162
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
#
|
|
163
|
+
breakdown = robots_discussing("PostgreSQL")
|
|
164
|
+
|
|
165
|
+
puts "Robots that discussed PostgreSQL:"
|
|
166
|
+
breakdown.each do |robot_name, count|
|
|
167
|
+
puts " #{robot_name}: #{count} mentions"
|
|
168
|
+
end
|
|
180
169
|
```
|
|
181
170
|
|
|
182
|
-
###
|
|
171
|
+
### Memory Timeline
|
|
183
172
|
|
|
184
|
-
See the chronological
|
|
173
|
+
See the chronological contributions across robots:
|
|
185
174
|
|
|
186
175
|
```ruby
|
|
187
|
-
|
|
176
|
+
# Get recent nodes with robot attribution
|
|
177
|
+
def memory_timeline(topic, limit: 50)
|
|
178
|
+
HTM::Models::Node
|
|
179
|
+
.where("content ILIKE ?", "%#{topic}%")
|
|
180
|
+
.order(created_at: :desc)
|
|
181
|
+
.limit(limit)
|
|
182
|
+
.includes(:robot_nodes)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
timeline = memory_timeline("architecture decisions", limit: 50)
|
|
188
186
|
|
|
189
187
|
puts "Architecture discussion timeline:"
|
|
190
|
-
timeline.each do |
|
|
191
|
-
|
|
192
|
-
|
|
188
|
+
timeline.each do |node|
|
|
189
|
+
robot_ids = node.robot_nodes.map(&:robot_id)
|
|
190
|
+
robots = HTM::Models::Robot.where(id: robot_ids).pluck(:name)
|
|
191
|
+
|
|
192
|
+
puts "#{node.created_at} - #{robots.join(', ')}"
|
|
193
|
+
puts " #{node.content[0..100]}..."
|
|
193
194
|
puts
|
|
194
195
|
end
|
|
195
196
|
```
|
|
@@ -203,59 +204,38 @@ Each robot has a specific role and expertise:
|
|
|
203
204
|
```ruby
|
|
204
205
|
class MultiRobotSystem
|
|
205
206
|
def initialize
|
|
206
|
-
@researcher = HTM.new(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
@developer = HTM.new(
|
|
212
|
-
robot_name: "Developer",
|
|
213
|
-
robot_id: "developer-001"
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
@reviewer = HTM.new(
|
|
217
|
-
robot_name: "Reviewer",
|
|
218
|
-
robot_id: "reviewer-001"
|
|
219
|
-
)
|
|
207
|
+
@researcher = HTM.new(robot_name: "Researcher")
|
|
208
|
+
@developer = HTM.new(robot_name: "Developer")
|
|
209
|
+
@reviewer = HTM.new(robot_name: "Reviewer")
|
|
220
210
|
end
|
|
221
211
|
|
|
222
212
|
def process_feature_request(feature)
|
|
223
213
|
# 1. Researcher gathers requirements
|
|
224
|
-
@researcher.
|
|
225
|
-
"research_#{feature}",
|
|
214
|
+
@researcher.remember(
|
|
226
215
|
"Research findings for #{feature}",
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
tags: ["research", feature]
|
|
216
|
+
tags: ["research", feature],
|
|
217
|
+
metadata: { category: "fact", priority: "high" }
|
|
230
218
|
)
|
|
231
219
|
|
|
232
220
|
# 2. Developer recalls research and implements
|
|
233
221
|
research = @developer.recall(
|
|
234
|
-
|
|
235
|
-
|
|
222
|
+
"research #{feature}",
|
|
223
|
+
timeframe: "last hour"
|
|
236
224
|
)
|
|
237
225
|
|
|
238
|
-
@developer.
|
|
239
|
-
"impl_#{feature}",
|
|
226
|
+
@developer.remember(
|
|
240
227
|
"Implementation plan based on research",
|
|
241
|
-
type: :decision,
|
|
242
|
-
importance: 9.0,
|
|
243
228
|
tags: ["implementation", feature],
|
|
244
|
-
|
|
229
|
+
metadata: { category: "decision", priority: "critical" }
|
|
245
230
|
)
|
|
246
231
|
|
|
247
232
|
# 3. Reviewer checks work
|
|
248
|
-
work = @reviewer.recall(
|
|
249
|
-
timeframe: "last hour",
|
|
250
|
-
topic: feature
|
|
251
|
-
)
|
|
233
|
+
work = @reviewer.recall(feature, timeframe: "last hour")
|
|
252
234
|
|
|
253
|
-
@reviewer.
|
|
254
|
-
"review_#{feature}",
|
|
235
|
+
@reviewer.remember(
|
|
255
236
|
"Code review findings",
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
tags: ["review", feature]
|
|
237
|
+
tags: ["review", feature],
|
|
238
|
+
metadata: { category: "context" }
|
|
259
239
|
)
|
|
260
240
|
end
|
|
261
241
|
end
|
|
@@ -275,15 +255,12 @@ class ShiftHandoff
|
|
|
275
255
|
end
|
|
276
256
|
|
|
277
257
|
def start_shift(shift_name)
|
|
278
|
-
@current_shift = HTM.new(
|
|
279
|
-
robot_name: "#{shift_name} Bot",
|
|
280
|
-
robot_id: "shift-#{shift_name.downcase}"
|
|
281
|
-
)
|
|
258
|
+
@current_shift = HTM.new(robot_name: "#{shift_name} Bot")
|
|
282
259
|
|
|
283
260
|
# Recall context from previous shift
|
|
284
261
|
handoff = @current_shift.recall(
|
|
262
|
+
"shift handoff urgent",
|
|
285
263
|
timeframe: "last 24 hours",
|
|
286
|
-
topic: "shift handoff urgent",
|
|
287
264
|
strategy: :hybrid,
|
|
288
265
|
limit: 20
|
|
289
266
|
)
|
|
@@ -296,12 +273,10 @@ class ShiftHandoff
|
|
|
296
273
|
|
|
297
274
|
def end_shift(summary)
|
|
298
275
|
# Document shift handoff
|
|
299
|
-
@current_shift.
|
|
300
|
-
"handoff_#{Time.now.to_i}",
|
|
276
|
+
@current_shift.remember(
|
|
301
277
|
summary,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
tags: ["shift-handoff", "urgent"]
|
|
278
|
+
tags: ["shift-handoff", "urgent"],
|
|
279
|
+
metadata: { category: "context", priority: "critical" }
|
|
305
280
|
)
|
|
306
281
|
|
|
307
282
|
puts "Shift handoff documented"
|
|
@@ -329,24 +304,12 @@ Specialized experts provide knowledge:
|
|
|
329
304
|
class ExpertSystem
|
|
330
305
|
def initialize
|
|
331
306
|
@experts = {
|
|
332
|
-
database: HTM.new(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
),
|
|
336
|
-
security: HTM.new(
|
|
337
|
-
robot_name: "Security Expert",
|
|
338
|
-
robot_id: "expert-security"
|
|
339
|
-
),
|
|
340
|
-
performance: HTM.new(
|
|
341
|
-
robot_name: "Performance Expert",
|
|
342
|
-
robot_id: "expert-performance"
|
|
343
|
-
)
|
|
307
|
+
database: HTM.new(robot_name: "Database Expert"),
|
|
308
|
+
security: HTM.new(robot_name: "Security Expert"),
|
|
309
|
+
performance: HTM.new(robot_name: "Performance Expert")
|
|
344
310
|
}
|
|
345
311
|
|
|
346
|
-
@general = HTM.new(
|
|
347
|
-
robot_name: "General Assistant",
|
|
348
|
-
robot_id: "assistant-general"
|
|
349
|
-
)
|
|
312
|
+
@general = HTM.new(robot_name: "General Assistant")
|
|
350
313
|
end
|
|
351
314
|
|
|
352
315
|
def consult(topic)
|
|
@@ -354,23 +317,21 @@ class ExpertSystem
|
|
|
354
317
|
expert_type = determine_expert(topic)
|
|
355
318
|
expert = @experts[expert_type]
|
|
356
319
|
|
|
357
|
-
# Get expert knowledge
|
|
320
|
+
# Get expert knowledge (use raw: true for full node data)
|
|
358
321
|
knowledge = expert.recall(
|
|
322
|
+
topic,
|
|
359
323
|
timeframe: "all time",
|
|
360
|
-
topic: topic,
|
|
361
324
|
strategy: :hybrid,
|
|
362
|
-
limit: 10
|
|
325
|
+
limit: 10,
|
|
326
|
+
raw: true
|
|
363
327
|
)
|
|
364
328
|
|
|
365
329
|
# General assistant learns from expert
|
|
366
330
|
knowledge.each do |k|
|
|
367
|
-
@general.
|
|
368
|
-
"
|
|
369
|
-
"Learned from #{expert_type} expert: #{k['value']}",
|
|
370
|
-
type: :fact,
|
|
371
|
-
importance: k['importance'],
|
|
331
|
+
@general.remember(
|
|
332
|
+
"Learned from #{expert_type} expert: #{k['content']}",
|
|
372
333
|
tags: ["learned", expert_type.to_s],
|
|
373
|
-
|
|
334
|
+
metadata: { category: "fact", source_id: k['id'] }
|
|
374
335
|
)
|
|
375
336
|
end
|
|
376
337
|
|
|
@@ -410,45 +371,38 @@ class CollaborativeDecision
|
|
|
410
371
|
end
|
|
411
372
|
|
|
412
373
|
def add_participant(name, role)
|
|
413
|
-
bot = HTM.new(
|
|
414
|
-
robot_name: "#{name} (#{role})",
|
|
415
|
-
robot_id: "decision-#{role.downcase}-#{SecureRandom.hex(4)}"
|
|
416
|
-
)
|
|
374
|
+
bot = HTM.new(robot_name: "#{name} (#{role})")
|
|
417
375
|
@participants << { name: name, role: role, bot: bot }
|
|
418
376
|
bot
|
|
419
377
|
end
|
|
420
378
|
|
|
421
379
|
def gather_input(bot, opinion)
|
|
422
|
-
bot.
|
|
423
|
-
"opinion_#{SecureRandom.hex(4)}",
|
|
380
|
+
bot.remember(
|
|
424
381
|
opinion,
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
tags: ["decision", @topic, "opinion"]
|
|
382
|
+
tags: ["decision", @topic, "opinion"],
|
|
383
|
+
metadata: { category: "context", priority: "high" }
|
|
428
384
|
)
|
|
429
385
|
end
|
|
430
386
|
|
|
431
387
|
def make_decision(decision_maker)
|
|
432
388
|
# Recall all opinions
|
|
433
389
|
opinions = decision_maker.recall(
|
|
390
|
+
"decision #{@topic} opinion",
|
|
434
391
|
timeframe: "last hour",
|
|
435
|
-
topic: "decision #{@topic} opinion",
|
|
436
392
|
strategy: :hybrid,
|
|
437
393
|
limit: 50
|
|
438
394
|
)
|
|
439
395
|
|
|
440
396
|
puts "#{decision_maker.robot_name} considering:"
|
|
441
397
|
opinions.each do |opinion|
|
|
442
|
-
puts "- #{opinion[
|
|
398
|
+
puts "- #{opinion[0..100]}..."
|
|
443
399
|
end
|
|
444
400
|
|
|
445
401
|
# Document final decision
|
|
446
|
-
decision_maker.
|
|
447
|
-
"decision_#{@topic}_final",
|
|
402
|
+
decision_maker.remember(
|
|
448
403
|
"Final decision on #{@topic} after considering team input",
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
tags: ["decision", @topic, "final"]
|
|
404
|
+
tags: ["decision", @topic, "final"],
|
|
405
|
+
metadata: { category: "decision", priority: "critical" }
|
|
452
406
|
)
|
|
453
407
|
end
|
|
454
408
|
end
|
|
@@ -475,57 +429,59 @@ decision.make_decision(lead)
|
|
|
475
429
|
|
|
476
430
|
### Sharing Strategies
|
|
477
431
|
|
|
478
|
-
Control what gets shared:
|
|
432
|
+
Control what gets shared using tags:
|
|
479
433
|
|
|
480
434
|
```ruby
|
|
481
435
|
class SmartSharing
|
|
482
|
-
def initialize(
|
|
483
|
-
@htm = HTM.new(robot_name:
|
|
484
|
-
@
|
|
436
|
+
def initialize(robot_name)
|
|
437
|
+
@htm = HTM.new(robot_name: robot_name)
|
|
438
|
+
@private_tag = "private:#{@htm.robot_id}"
|
|
485
439
|
end
|
|
486
440
|
|
|
487
|
-
def add_shared(
|
|
488
|
-
# Shared with all robots
|
|
489
|
-
@htm.
|
|
490
|
-
tags: (opts[:tags] || []) + ["shared"]
|
|
491
|
-
|
|
441
|
+
def add_shared(value, **opts)
|
|
442
|
+
# Shared with all robots - use "shared" tag
|
|
443
|
+
@htm.remember(value,
|
|
444
|
+
tags: (opts[:tags] || []) + ["shared"],
|
|
445
|
+
metadata: opts[:metadata] || {}
|
|
446
|
+
)
|
|
492
447
|
end
|
|
493
448
|
|
|
494
|
-
def add_private(
|
|
495
|
-
#
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
))
|
|
449
|
+
def add_private(value, **opts)
|
|
450
|
+
# Private to this robot - use robot-specific tag
|
|
451
|
+
@htm.remember(value,
|
|
452
|
+
tags: (opts[:tags] || []) + [@private_tag],
|
|
453
|
+
metadata: opts[:metadata] || {}
|
|
454
|
+
)
|
|
501
455
|
end
|
|
502
456
|
|
|
503
457
|
def recall_shared(topic)
|
|
504
|
-
#
|
|
458
|
+
# Query with shared tag filter
|
|
505
459
|
@htm.recall(
|
|
460
|
+
"shared #{topic}",
|
|
506
461
|
timeframe: "all time",
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
)
|
|
462
|
+
strategy: :hybrid,
|
|
463
|
+
query_tags: ["shared"]
|
|
464
|
+
)
|
|
510
465
|
end
|
|
511
466
|
|
|
512
467
|
def recall_private(topic)
|
|
513
|
-
#
|
|
468
|
+
# Query with private tag filter
|
|
514
469
|
@htm.recall(
|
|
470
|
+
topic,
|
|
515
471
|
timeframe: "all time",
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
)
|
|
472
|
+
strategy: :hybrid,
|
|
473
|
+
query_tags: [@private_tag]
|
|
474
|
+
)
|
|
519
475
|
end
|
|
520
476
|
end
|
|
521
477
|
|
|
522
478
|
# Usage
|
|
523
|
-
bot1 = SmartSharing.new("
|
|
524
|
-
bot1.add_shared("
|
|
525
|
-
bot1.add_private("
|
|
479
|
+
bot1 = SmartSharing.new("Bot 001")
|
|
480
|
+
bot1.add_shared("Everyone should know this", metadata: { category: "fact" })
|
|
481
|
+
bot1.add_private("Private thought", metadata: { category: "context" })
|
|
526
482
|
|
|
527
|
-
bot2 = SmartSharing.new("
|
|
528
|
-
shared = bot2.recall_shared("fact") # Can see
|
|
483
|
+
bot2 = SmartSharing.new("Bot 002")
|
|
484
|
+
shared = bot2.recall_shared("fact") # Can see shared content
|
|
529
485
|
private = bot2.recall_private("thought") # Won't see bot1's private thoughts
|
|
530
486
|
```
|
|
531
487
|
|
|
@@ -534,39 +490,30 @@ private = bot2.recall_private("thought") # Won't see bot1's private thoughts
|
|
|
534
490
|
### Finding Robot Activity
|
|
535
491
|
|
|
536
492
|
```ruby
|
|
537
|
-
# Get all robots and their activity
|
|
493
|
+
# Get all robots and their activity using ActiveRecord
|
|
538
494
|
def get_robot_activity
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
result = conn.exec(
|
|
543
|
-
<<~SQL
|
|
544
|
-
SELECT
|
|
545
|
-
r.id,
|
|
546
|
-
r.name,
|
|
547
|
-
COUNT(n.id) as memory_count,
|
|
548
|
-
MAX(n.created_at) as last_memory,
|
|
549
|
-
r.last_active
|
|
550
|
-
FROM robots r
|
|
551
|
-
LEFT JOIN nodes n ON r.id = n.robot_id
|
|
552
|
-
GROUP BY r.id, r.name, r.last_active
|
|
553
|
-
ORDER BY r.last_active DESC
|
|
554
|
-
SQL
|
|
555
|
-
)
|
|
495
|
+
HTM::Models::Robot.all.map do |robot|
|
|
496
|
+
memory_count = robot.robot_nodes.count
|
|
497
|
+
last_memory = robot.robot_nodes.maximum(:last_remembered_at)
|
|
556
498
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
499
|
+
{
|
|
500
|
+
id: robot.id,
|
|
501
|
+
name: robot.name,
|
|
502
|
+
memory_count: memory_count,
|
|
503
|
+
last_memory: last_memory,
|
|
504
|
+
last_active_at: robot.last_active_at
|
|
505
|
+
}
|
|
506
|
+
end.sort_by { |r| r[:last_active_at] || Time.at(0) }.reverse
|
|
560
507
|
end
|
|
561
508
|
|
|
562
509
|
# Display activity
|
|
563
510
|
robots = get_robot_activity
|
|
564
511
|
puts "Robot Activity Report:"
|
|
565
512
|
robots.each do |r|
|
|
566
|
-
puts "\n#{r[
|
|
567
|
-
puts " Memories: #{r[
|
|
568
|
-
puts " Last memory: #{r[
|
|
569
|
-
puts " Last active: #{r[
|
|
513
|
+
puts "\n#{r[:name]} (#{r[:id]})"
|
|
514
|
+
puts " Memories: #{r[:memory_count]}"
|
|
515
|
+
puts " Last memory: #{r[:last_memory]}"
|
|
516
|
+
puts " Last active: #{r[:last_active_at]}"
|
|
570
517
|
end
|
|
571
518
|
```
|
|
572
519
|
|
|
@@ -574,37 +521,24 @@ end
|
|
|
574
521
|
|
|
575
522
|
```ruby
|
|
576
523
|
def search_across_robots(topic, limit_per_robot: 5)
|
|
577
|
-
config = HTM::Database.default_config
|
|
578
|
-
conn = PG.connect(config)
|
|
579
|
-
|
|
580
|
-
# Get all robots
|
|
581
|
-
robots = conn.exec("SELECT id, name FROM robots")
|
|
582
|
-
|
|
583
524
|
results = {}
|
|
584
525
|
|
|
585
|
-
|
|
586
|
-
#
|
|
587
|
-
|
|
588
|
-
"search_#{robot['id']}",
|
|
589
|
-
<<~SQL
|
|
590
|
-
SELECT key, value, type, importance, created_at
|
|
591
|
-
FROM nodes
|
|
592
|
-
WHERE robot_id = $1
|
|
593
|
-
AND to_tsvector('english', value) @@ plainto_tsquery('english', $2)
|
|
594
|
-
ORDER BY importance DESC
|
|
595
|
-
LIMIT $3
|
|
596
|
-
SQL
|
|
597
|
-
)
|
|
526
|
+
HTM::Models::Robot.find_each do |robot|
|
|
527
|
+
# Find nodes linked to this robot via robot_nodes
|
|
528
|
+
node_ids = robot.robot_nodes.pluck(:node_id)
|
|
598
529
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
530
|
+
# Search within this robot's nodes using full-text search
|
|
531
|
+
robot_nodes = HTM::Models::Node
|
|
532
|
+
.where(id: node_ids)
|
|
533
|
+
.where("to_tsvector('english', content) @@ plainto_tsquery('english', ?)", topic)
|
|
534
|
+
.order(created_at: :desc)
|
|
535
|
+
.limit(limit_per_robot)
|
|
603
536
|
|
|
604
|
-
results[robot
|
|
537
|
+
results[robot.name] = robot_nodes.map do |n|
|
|
538
|
+
{ id: n.id, content: n.content, created_at: n.created_at }
|
|
539
|
+
end
|
|
605
540
|
end
|
|
606
541
|
|
|
607
|
-
conn.close
|
|
608
542
|
results
|
|
609
543
|
end
|
|
610
544
|
|
|
@@ -613,7 +547,7 @@ results = search_across_robots("authentication")
|
|
|
613
547
|
results.each do |robot_name, memories|
|
|
614
548
|
puts "\n=== #{robot_name} ==="
|
|
615
549
|
memories.each do |m|
|
|
616
|
-
puts "-
|
|
550
|
+
puts "- #{m[:content][0..80]}..."
|
|
617
551
|
end
|
|
618
552
|
end
|
|
619
553
|
```
|
|
@@ -624,33 +558,21 @@ end
|
|
|
624
558
|
|
|
625
559
|
```ruby
|
|
626
560
|
class MultiRobotDashboard
|
|
627
|
-
def initialize
|
|
628
|
-
@config = HTM::Database.default_config
|
|
629
|
-
end
|
|
630
|
-
|
|
631
561
|
def summary
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
FROM robots r
|
|
647
|
-
LEFT JOIN nodes n ON r.id = n.robot_id
|
|
648
|
-
GROUP BY r.id, r.name
|
|
649
|
-
ORDER BY memories DESC
|
|
650
|
-
SQL
|
|
651
|
-
).to_a
|
|
652
|
-
|
|
653
|
-
conn.close
|
|
562
|
+
total_robots = HTM::Models::Robot.count
|
|
563
|
+
total_memories = HTM::Models::Node.count
|
|
564
|
+
|
|
565
|
+
# Per-robot breakdown using ActiveRecord
|
|
566
|
+
breakdown = HTM::Models::Robot.all.map do |robot|
|
|
567
|
+
robot_node_ids = robot.robot_nodes.pluck(:node_id)
|
|
568
|
+
nodes = HTM::Models::Node.where(id: robot_node_ids)
|
|
569
|
+
|
|
570
|
+
{
|
|
571
|
+
name: robot.name,
|
|
572
|
+
memories: nodes.count,
|
|
573
|
+
last_contribution: robot.robot_nodes.maximum(:last_remembered_at)
|
|
574
|
+
}
|
|
575
|
+
end.sort_by { |r| -r[:memories] }
|
|
654
576
|
|
|
655
577
|
{
|
|
656
578
|
total_robots: total_robots,
|
|
@@ -668,10 +590,9 @@ class MultiRobotDashboard
|
|
|
668
590
|
puts "\nPer-robot breakdown:"
|
|
669
591
|
|
|
670
592
|
data[:breakdown].each do |robot|
|
|
671
|
-
puts "\n#{robot[
|
|
672
|
-
puts " Memories: #{robot[
|
|
673
|
-
puts "
|
|
674
|
-
puts " Last contribution: #{robot['last_contribution']}"
|
|
593
|
+
puts "\n#{robot[:name]}"
|
|
594
|
+
puts " Memories: #{robot[:memories]}"
|
|
595
|
+
puts " Last contribution: #{robot[:last_contribution]}"
|
|
675
596
|
end
|
|
676
597
|
end
|
|
677
598
|
end
|
|
@@ -686,11 +607,11 @@ dashboard.print_summary
|
|
|
686
607
|
|
|
687
608
|
```ruby
|
|
688
609
|
# Good: Clear, specific roles
|
|
689
|
-
researcher = HTM.new(robot_name: "Research Specialist"
|
|
690
|
-
coder = HTM.new(robot_name: "Code Generator"
|
|
610
|
+
researcher = HTM.new(robot_name: "Research Specialist")
|
|
611
|
+
coder = HTM.new(robot_name: "Code Generator")
|
|
691
612
|
|
|
692
613
|
# Avoid: Vague roles
|
|
693
|
-
bot1 = HTM.new(robot_name: "Bot 1"
|
|
614
|
+
bot1 = HTM.new(robot_name: "Bot 1")
|
|
694
615
|
```
|
|
695
616
|
|
|
696
617
|
### 2. Consistent Naming
|
|
@@ -698,27 +619,23 @@ bot1 = HTM.new(robot_name: "Bot 1", robot_id: "bot1")
|
|
|
698
619
|
```ruby
|
|
699
620
|
# Good: Consistent naming scheme
|
|
700
621
|
class RobotFactory
|
|
701
|
-
def self.create(service, purpose
|
|
702
|
-
HTM.new(
|
|
703
|
-
robot_name: "#{service.capitalize} #{purpose.capitalize}",
|
|
704
|
-
robot_id: "#{service}-#{purpose}-#{instance}"
|
|
705
|
-
)
|
|
622
|
+
def self.create(service, purpose)
|
|
623
|
+
HTM.new(robot_name: "#{service.capitalize} #{purpose.capitalize}")
|
|
706
624
|
end
|
|
707
625
|
end
|
|
708
626
|
|
|
709
|
-
api_assistant = RobotFactory.create("api", "assistant"
|
|
710
|
-
api_validator = RobotFactory.create("api", "validator"
|
|
627
|
+
api_assistant = RobotFactory.create("api", "assistant")
|
|
628
|
+
api_validator = RobotFactory.create("api", "validator")
|
|
711
629
|
```
|
|
712
630
|
|
|
713
631
|
### 3. Attribution in Content
|
|
714
632
|
|
|
715
633
|
```ruby
|
|
716
634
|
# Include attribution in the content itself
|
|
717
|
-
bot.
|
|
718
|
-
"finding_001",
|
|
635
|
+
bot.remember(
|
|
719
636
|
"Research by #{bot.robot_name}: PostgreSQL outperforms MongoDB",
|
|
720
|
-
|
|
721
|
-
|
|
637
|
+
tags: ["research", "database:comparison"],
|
|
638
|
+
metadata: { category: "fact", priority: "high" }
|
|
722
639
|
)
|
|
723
640
|
```
|
|
724
641
|
|
|
@@ -727,13 +644,14 @@ bot.add_node(
|
|
|
727
644
|
```ruby
|
|
728
645
|
# Periodically sync understanding across robots
|
|
729
646
|
def sync_robots(*robots)
|
|
730
|
-
# Find recent high-
|
|
647
|
+
# Find recent high-priority memories using metadata
|
|
731
648
|
shared_knowledge = robots.first.recall(
|
|
649
|
+
"important shared",
|
|
732
650
|
timeframe: "last 24 hours",
|
|
733
|
-
topic: "important shared",
|
|
734
651
|
strategy: :hybrid,
|
|
735
|
-
limit: 50
|
|
736
|
-
|
|
652
|
+
limit: 50,
|
|
653
|
+
metadata: { priority: "high" }
|
|
654
|
+
)
|
|
737
655
|
|
|
738
656
|
puts "Syncing #{shared_knowledge.length} important memories across #{robots.length} robots"
|
|
739
657
|
end
|
|
@@ -743,22 +661,14 @@ end
|
|
|
743
661
|
|
|
744
662
|
```ruby
|
|
745
663
|
def cleanup_inactive_robots(days: 30)
|
|
746
|
-
config = HTM::Database.default_config
|
|
747
|
-
conn = PG.connect(config)
|
|
748
|
-
|
|
749
664
|
cutoff = Time.now - (days * 24 * 3600)
|
|
750
665
|
|
|
751
|
-
|
|
752
|
-
"SELECT id, name FROM robots WHERE last_active < $1",
|
|
753
|
-
[cutoff]
|
|
754
|
-
)
|
|
666
|
+
inactive = HTM::Models::Robot.where("last_active_at < ?", cutoff)
|
|
755
667
|
|
|
756
668
|
puts "Inactive robots (last active > #{days} days):"
|
|
757
|
-
|
|
758
|
-
puts "- #{robot
|
|
669
|
+
inactive.each do |robot|
|
|
670
|
+
puts "- #{robot.name} (#{robot.id})"
|
|
759
671
|
end
|
|
760
|
-
|
|
761
|
-
conn.close
|
|
762
672
|
end
|
|
763
673
|
|
|
764
674
|
cleanup_inactive_robots(days: 90)
|
|
@@ -772,20 +682,9 @@ require 'htm'
|
|
|
772
682
|
# Create a multi-robot development team
|
|
773
683
|
class DevTeam
|
|
774
684
|
def initialize
|
|
775
|
-
@analyst = HTM.new(
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
)
|
|
779
|
-
|
|
780
|
-
@developer = HTM.new(
|
|
781
|
-
robot_name: "Senior Developer",
|
|
782
|
-
robot_id: "team-developer-001"
|
|
783
|
-
)
|
|
784
|
-
|
|
785
|
-
@tester = HTM.new(
|
|
786
|
-
robot_name: "QA Tester",
|
|
787
|
-
robot_id: "team-tester-001"
|
|
788
|
-
)
|
|
685
|
+
@analyst = HTM.new(robot_name: "Requirements Analyst")
|
|
686
|
+
@developer = HTM.new(robot_name: "Senior Developer")
|
|
687
|
+
@tester = HTM.new(robot_name: "QA Tester")
|
|
789
688
|
end
|
|
790
689
|
|
|
791
690
|
def process_feature(feature_name)
|
|
@@ -793,63 +692,68 @@ class DevTeam
|
|
|
793
692
|
|
|
794
693
|
# 1. Analyst documents requirements
|
|
795
694
|
puts "\n1. Analyst gathering requirements..."
|
|
796
|
-
@analyst.
|
|
797
|
-
"req_#{feature_name}",
|
|
695
|
+
@analyst.remember(
|
|
798
696
|
"Requirements for #{feature_name}: Must support OAuth2",
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
tags: ["requirements", feature_name]
|
|
697
|
+
tags: ["requirements", feature_name],
|
|
698
|
+
metadata: { category: "fact", priority: "critical" }
|
|
802
699
|
)
|
|
803
700
|
|
|
804
701
|
# 2. Developer recalls requirements and designs
|
|
805
702
|
puts "\n2. Developer reviewing requirements..."
|
|
806
703
|
requirements = @developer.recall(
|
|
807
|
-
|
|
808
|
-
|
|
704
|
+
"requirements #{feature_name}",
|
|
705
|
+
timeframe: "last hour"
|
|
809
706
|
)
|
|
810
707
|
|
|
811
708
|
puts "Found #{requirements.length} requirements"
|
|
812
709
|
|
|
813
|
-
@developer.
|
|
814
|
-
"design_#{feature_name}",
|
|
710
|
+
@developer.remember(
|
|
815
711
|
"Design for #{feature_name} based on requirements",
|
|
816
|
-
type: :decision,
|
|
817
|
-
importance: 9.0,
|
|
818
712
|
tags: ["design", feature_name],
|
|
819
|
-
|
|
713
|
+
metadata: { category: "decision", priority: "critical" }
|
|
820
714
|
)
|
|
821
715
|
|
|
822
716
|
# 3. Tester recalls everything and creates test plan
|
|
823
717
|
puts "\n3. Tester creating test plan..."
|
|
824
718
|
context = @tester.recall(
|
|
719
|
+
feature_name,
|
|
825
720
|
timeframe: "last hour",
|
|
826
|
-
topic: feature_name,
|
|
827
721
|
strategy: :hybrid
|
|
828
722
|
)
|
|
829
723
|
|
|
830
724
|
puts "Tester reviewed #{context.length} items"
|
|
831
725
|
|
|
832
|
-
@tester.
|
|
833
|
-
"test_#{feature_name}",
|
|
726
|
+
@tester.remember(
|
|
834
727
|
"Test plan for #{feature_name}",
|
|
835
|
-
type: :context,
|
|
836
|
-
importance: 8.0,
|
|
837
728
|
tags: ["testing", feature_name],
|
|
838
|
-
|
|
729
|
+
metadata: { category: "context", priority: "high" }
|
|
839
730
|
)
|
|
840
731
|
|
|
841
|
-
# 4. Show collaboration
|
|
732
|
+
# 4. Show collaboration timeline
|
|
842
733
|
puts "\n4. Collaboration summary:"
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
734
|
+
nodes = HTM::Models::Node
|
|
735
|
+
.where("content ILIKE ?", "%#{feature_name}%")
|
|
736
|
+
.order(created_at: :asc)
|
|
737
|
+
.includes(:robot_nodes)
|
|
738
|
+
|
|
739
|
+
nodes.each do |node|
|
|
740
|
+
robot_ids = node.robot_nodes.map(&:robot_id)
|
|
741
|
+
robots = HTM::Models::Robot.where(id: robot_ids).pluck(:name)
|
|
742
|
+
puts "- #{robots.join(', ')}: #{node.content[0..50]}..."
|
|
846
743
|
end
|
|
847
744
|
|
|
848
745
|
# 5. Show attribution
|
|
849
746
|
puts "\n5. Who contributed:"
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
747
|
+
robot_counts = Hash.new(0)
|
|
748
|
+
nodes.each do |node|
|
|
749
|
+
node.robot_nodes.each do |rn|
|
|
750
|
+
robot = HTM::Models::Robot.find(rn.robot_id)
|
|
751
|
+
robot_counts[robot.name] += 1
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
robot_counts.each do |robot_name, count|
|
|
756
|
+
puts "- #{robot_name}: #{count} memories"
|
|
853
757
|
end
|
|
854
758
|
end
|
|
855
759
|
end
|