kbs 0.0.1 → 0.1.0
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/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +68 -2
- data/README.md +235 -334
- data/docs/DOCUMENTATION_STATUS.md +158 -0
- data/docs/advanced/custom-persistence.md +775 -0
- data/docs/advanced/debugging.md +726 -0
- data/docs/advanced/index.md +8 -0
- data/docs/advanced/performance.md +832 -0
- data/docs/advanced/testing.md +691 -0
- data/docs/api/blackboard.md +1157 -0
- data/docs/api/engine.md +978 -0
- data/docs/api/facts.md +1212 -0
- data/docs/api/index.md +12 -0
- data/docs/api/rules.md +1034 -0
- data/docs/architecture/blackboard.md +553 -0
- data/docs/architecture/index.md +277 -0
- data/docs/architecture/network-structure.md +343 -0
- data/docs/architecture/rete-algorithm.md +737 -0
- data/docs/assets/css/custom.css +83 -0
- data/docs/assets/images/blackboard-architecture.svg +136 -0
- data/docs/assets/images/compiled-network.svg +101 -0
- data/docs/assets/images/fact-assertion-flow.svg +117 -0
- data/docs/assets/images/kbs.jpg +0 -0
- data/docs/assets/images/pattern-matching-trace.svg +136 -0
- data/docs/assets/images/rete-network-layers.svg +96 -0
- data/docs/assets/images/system-layers.svg +69 -0
- data/docs/assets/images/trading-signal-network.svg +139 -0
- data/docs/assets/js/mathjax.js +17 -0
- data/docs/examples/expert-systems.md +1031 -0
- data/docs/examples/index.md +9 -0
- data/docs/examples/multi-agent.md +1335 -0
- data/docs/examples/stock-trading.md +488 -0
- data/docs/guides/blackboard-memory.md +558 -0
- data/docs/guides/dsl.md +1321 -0
- data/docs/guides/facts.md +652 -0
- data/docs/guides/getting-started.md +383 -0
- data/docs/guides/index.md +23 -0
- data/docs/guides/negation.md +529 -0
- data/docs/guides/pattern-matching.md +561 -0
- data/docs/guides/persistence.md +451 -0
- data/docs/guides/variable-binding.md +491 -0
- data/docs/guides/writing-rules.md +755 -0
- data/docs/index.md +157 -0
- data/docs/installation.md +156 -0
- data/docs/quick-start.md +228 -0
- data/examples/README.md +2 -2
- data/examples/advanced_example.rb +2 -2
- data/examples/advanced_example_dsl.rb +224 -0
- data/examples/ai_enhanced_kbs.rb +1 -1
- data/examples/ai_enhanced_kbs_dsl.rb +538 -0
- data/examples/blackboard_demo_dsl.rb +50 -0
- data/examples/car_diagnostic.rb +1 -1
- data/examples/car_diagnostic_dsl.rb +54 -0
- data/examples/concurrent_inference_demo.rb +5 -5
- data/examples/concurrent_inference_demo_dsl.rb +363 -0
- data/examples/csv_trading_system.rb +1 -1
- data/examples/csv_trading_system_dsl.rb +525 -0
- data/examples/knowledge_base.db +0 -0
- data/examples/portfolio_rebalancing_system.rb +2 -2
- data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
- data/examples/redis_trading_demo_dsl.rb +177 -0
- data/examples/run_all.rb +50 -0
- data/examples/run_all_dsl.rb +49 -0
- data/examples/stock_trading_advanced.rb +1 -1
- data/examples/stock_trading_advanced_dsl.rb +404 -0
- data/examples/temp.txt +7693 -0
- data/examples/temp_dsl.txt +8447 -0
- data/examples/timestamped_trading.rb +1 -1
- data/examples/timestamped_trading_dsl.rb +258 -0
- data/examples/trading_demo.rb +1 -1
- data/examples/trading_demo_dsl.rb +322 -0
- data/examples/working_demo.rb +1 -1
- data/examples/working_demo_dsl.rb +160 -0
- data/lib/kbs/blackboard/engine.rb +3 -3
- data/lib/kbs/blackboard/fact.rb +1 -1
- data/lib/kbs/condition.rb +1 -1
- data/lib/kbs/dsl/knowledge_base.rb +1 -1
- data/lib/kbs/dsl/variable.rb +1 -1
- data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
- data/lib/kbs/fact.rb +1 -1
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +2 -2
- data/mkdocs.yml +181 -0
- metadata +66 -6
- data/examples/stock_trading_system.rb.bak +0 -563
|
@@ -0,0 +1,1157 @@
|
|
|
1
|
+
# Blackboard API Reference
|
|
2
|
+
|
|
3
|
+
Complete API reference for blackboard memory classes.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [KBS::Blackboard::Memory](#kbsblackboardmemory) - Central blackboard workspace
|
|
8
|
+
- [KBS::Blackboard::MessageQueue](#kbsblackboardmessagequeue) - Inter-agent communication
|
|
9
|
+
- [KBS::Blackboard::AuditLog](#kbsblackboardauditlog) - Historical tracking
|
|
10
|
+
- [Usage Patterns](#usage-patterns)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## KBS::Blackboard::Memory
|
|
15
|
+
|
|
16
|
+
The central blackboard workspace that coordinates facts, messages, and audit logging.
|
|
17
|
+
|
|
18
|
+
**Architecture**: Composes three components:
|
|
19
|
+
1. **Store** - Persistence layer (SQLite, Redis, or Hybrid)
|
|
20
|
+
2. **MessageQueue** - Priority-based inter-agent messaging
|
|
21
|
+
3. **AuditLog** - Complete history of fact changes and rule firings
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
### Constructor
|
|
26
|
+
|
|
27
|
+
#### `initialize(db_path: ':memory:', store: nil)`
|
|
28
|
+
|
|
29
|
+
Creates a new blackboard memory.
|
|
30
|
+
|
|
31
|
+
**Parameters**:
|
|
32
|
+
- `db_path` (String, optional) - Path to SQLite database (default: `:memory:`)
|
|
33
|
+
- `store` (KBS::Blackboard::Persistence::Store, optional) - Custom store (default: `nil`, creates SQLiteStore)
|
|
34
|
+
|
|
35
|
+
**Returns**: `KBS::Blackboard::Memory` instance
|
|
36
|
+
|
|
37
|
+
**Side Effects**:
|
|
38
|
+
- Generates session UUID
|
|
39
|
+
- Creates or connects to persistence store
|
|
40
|
+
- Initializes message queue and audit log
|
|
41
|
+
- Sets up database tables/indexes
|
|
42
|
+
|
|
43
|
+
**Example - In-Memory**:
|
|
44
|
+
```ruby
|
|
45
|
+
memory = KBS::Blackboard::Memory.new
|
|
46
|
+
# Blackboard stored in RAM (lost on exit)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Example - SQLite Persistence**:
|
|
50
|
+
```ruby
|
|
51
|
+
memory = KBS::Blackboard::Memory.new(db_path: 'knowledge_base.db')
|
|
52
|
+
# Facts persisted to knowledge_base.db
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Example - Redis Store**:
|
|
56
|
+
```ruby
|
|
57
|
+
require 'kbs/blackboard/persistence/redis_store'
|
|
58
|
+
|
|
59
|
+
store = KBS::Blackboard::Persistence::RedisStore.new(url: 'redis://localhost:6379/0')
|
|
60
|
+
memory = KBS::Blackboard::Memory.new(store: store)
|
|
61
|
+
# Fast, distributed persistence
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Example - Hybrid Store**:
|
|
65
|
+
```ruby
|
|
66
|
+
require 'kbs/blackboard/persistence/hybrid_store'
|
|
67
|
+
|
|
68
|
+
store = KBS::Blackboard::Persistence::HybridStore.new(
|
|
69
|
+
redis_url: 'redis://localhost:6379/0',
|
|
70
|
+
db_path: 'audit.db'
|
|
71
|
+
)
|
|
72
|
+
memory = KBS::Blackboard::Memory.new(store: store)
|
|
73
|
+
# Facts in Redis, audit trail in SQLite
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### Public Attributes
|
|
79
|
+
|
|
80
|
+
#### `session_id`
|
|
81
|
+
|
|
82
|
+
**Type**: `String`
|
|
83
|
+
|
|
84
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
85
|
+
|
|
86
|
+
**Description**: Unique session identifier (UUID)
|
|
87
|
+
|
|
88
|
+
**Example**:
|
|
89
|
+
```ruby
|
|
90
|
+
memory = KBS::Blackboard::Memory.new
|
|
91
|
+
puts memory.session_id # => "550e8400-e29b-41d4-a716-446655440000"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Use Cases**:
|
|
95
|
+
- Filter audit log by session
|
|
96
|
+
- Separate facts from different runs
|
|
97
|
+
- Debugging multi-session scenarios
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
#### `store`
|
|
102
|
+
|
|
103
|
+
**Type**: `KBS::Blackboard::Persistence::Store`
|
|
104
|
+
|
|
105
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
106
|
+
|
|
107
|
+
**Description**: The underlying persistence store
|
|
108
|
+
|
|
109
|
+
**Example**:
|
|
110
|
+
```ruby
|
|
111
|
+
memory = KBS::Blackboard::Memory.new(db_path: 'kb.db')
|
|
112
|
+
puts memory.store.class # => KBS::Blackboard::Persistence::SqliteStore
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
#### `message_queue`
|
|
118
|
+
|
|
119
|
+
**Type**: `KBS::Blackboard::MessageQueue`
|
|
120
|
+
|
|
121
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
122
|
+
|
|
123
|
+
**Description**: The message queue for inter-agent communication
|
|
124
|
+
|
|
125
|
+
**Example**:
|
|
126
|
+
```ruby
|
|
127
|
+
memory.message_queue.post("agent1", "alerts", { level: "critical" })
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
#### `audit_log`
|
|
133
|
+
|
|
134
|
+
**Type**: `KBS::Blackboard::AuditLog`
|
|
135
|
+
|
|
136
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
137
|
+
|
|
138
|
+
**Description**: The audit log for tracking all changes
|
|
139
|
+
|
|
140
|
+
**Example**:
|
|
141
|
+
```ruby
|
|
142
|
+
history = memory.audit_log.fact_history(fact.uuid)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### Fact Management Methods
|
|
148
|
+
|
|
149
|
+
#### `add_fact(type, attributes = {})`
|
|
150
|
+
|
|
151
|
+
Adds a persistent fact to the blackboard.
|
|
152
|
+
|
|
153
|
+
**Parameters**:
|
|
154
|
+
- `type` (Symbol) - Fact type
|
|
155
|
+
- `attributes` (Hash, optional) - Fact attributes (default: `{}`)
|
|
156
|
+
|
|
157
|
+
**Returns**: `KBS::Blackboard::Fact` - Persistent fact with UUID
|
|
158
|
+
|
|
159
|
+
**Side Effects**:
|
|
160
|
+
- Generates UUID for fact
|
|
161
|
+
- Saves fact to store (within transaction)
|
|
162
|
+
- Logs addition to audit log
|
|
163
|
+
- Notifies observers
|
|
164
|
+
|
|
165
|
+
**Example**:
|
|
166
|
+
```ruby
|
|
167
|
+
fact = memory.add_fact(:temperature, location: "server_room", value: 85)
|
|
168
|
+
puts fact.uuid # => "550e8400-e29b-41d4-a716-446655440000"
|
|
169
|
+
puts fact.type # => :temperature
|
|
170
|
+
puts fact[:value] # => 85
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Transaction Handling**:
|
|
174
|
+
```ruby
|
|
175
|
+
memory.transaction do
|
|
176
|
+
fact1 = memory.add_fact(:order, id: 1, status: "pending")
|
|
177
|
+
fact2 = memory.add_fact(:inventory, item: "ABC", quantity: 100)
|
|
178
|
+
# Both facts committed together
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
#### `remove_fact(fact)`
|
|
185
|
+
|
|
186
|
+
Removes a fact from the blackboard.
|
|
187
|
+
|
|
188
|
+
**Parameters**:
|
|
189
|
+
- `fact` (KBS::Blackboard::Fact or String) - Fact object or UUID
|
|
190
|
+
|
|
191
|
+
**Returns**: `nil`
|
|
192
|
+
|
|
193
|
+
**Side Effects**:
|
|
194
|
+
- Marks fact as inactive in store
|
|
195
|
+
- Logs removal to audit log
|
|
196
|
+
- Notifies observers
|
|
197
|
+
|
|
198
|
+
**Example**:
|
|
199
|
+
```ruby
|
|
200
|
+
fact = memory.add_fact(:temperature, value: 85)
|
|
201
|
+
memory.remove_fact(fact)
|
|
202
|
+
|
|
203
|
+
# Or by UUID
|
|
204
|
+
memory.remove_fact("550e8400-e29b-41d4-a716-446655440000")
|
|
205
|
+
|
|
206
|
+
# Fact remains in audit log
|
|
207
|
+
history = memory.get_history(fact.uuid)
|
|
208
|
+
puts history.last[:action] # => "REMOVE"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
#### `update_fact(fact, new_attributes)`
|
|
214
|
+
|
|
215
|
+
Updates a fact's attributes.
|
|
216
|
+
|
|
217
|
+
**Parameters**:
|
|
218
|
+
- `fact` (KBS::Blackboard::Fact or String) - Fact object or UUID
|
|
219
|
+
- `new_attributes` (Hash) - New attributes to merge
|
|
220
|
+
|
|
221
|
+
**Returns**: `nil`
|
|
222
|
+
|
|
223
|
+
**Side Effects**:
|
|
224
|
+
- Updates fact in store
|
|
225
|
+
- Logs update to audit log
|
|
226
|
+
|
|
227
|
+
**Example**:
|
|
228
|
+
```ruby
|
|
229
|
+
fact = memory.add_fact(:temperature, location: "server_room", value: 85)
|
|
230
|
+
memory.update_fact(fact, value: 90, timestamp: Time.now)
|
|
231
|
+
|
|
232
|
+
# Or by UUID
|
|
233
|
+
memory.update_fact(fact.uuid, value: 95)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Note**: Updates do NOT notify observers or trigger rule re-evaluation. For that, retract and re-add the fact.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
#### `get_facts(type = nil, pattern = {})`
|
|
241
|
+
|
|
242
|
+
Retrieves facts from the blackboard.
|
|
243
|
+
|
|
244
|
+
**Parameters**:
|
|
245
|
+
- `type` (Symbol, optional) - Filter by fact type (default: `nil`, all types)
|
|
246
|
+
- `pattern` (Hash, optional) - Additional attribute filters (default: `{}`)
|
|
247
|
+
|
|
248
|
+
**Returns**: `Array<KBS::Blackboard::Fact>`
|
|
249
|
+
|
|
250
|
+
**Example**:
|
|
251
|
+
```ruby
|
|
252
|
+
# Get all facts
|
|
253
|
+
all_facts = memory.get_facts
|
|
254
|
+
|
|
255
|
+
# Get all temperature facts
|
|
256
|
+
temps = memory.get_facts(:temperature)
|
|
257
|
+
|
|
258
|
+
# Get temperature facts from specific location
|
|
259
|
+
server_temps = memory.get_facts(:temperature, location: "server_room")
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Performance**: O(N) where N = total facts (uses linear scan). For large datasets, consider `query_facts`.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
#### `facts`
|
|
267
|
+
|
|
268
|
+
Alias for `get_facts()`. Returns all facts.
|
|
269
|
+
|
|
270
|
+
**Returns**: `Array<KBS::Blackboard::Fact>`
|
|
271
|
+
|
|
272
|
+
**Example**:
|
|
273
|
+
```ruby
|
|
274
|
+
puts "Total facts: #{memory.facts.size}"
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
#### `query_facts(sql_conditions = nil, params = [])`
|
|
280
|
+
|
|
281
|
+
Advanced SQL query for facts (SQLite store only).
|
|
282
|
+
|
|
283
|
+
**Parameters**:
|
|
284
|
+
- `sql_conditions` (String, optional) - SQL WHERE clause (default: `nil`)
|
|
285
|
+
- `params` (Array, optional) - Parameters for SQL query (default: `[]`)
|
|
286
|
+
|
|
287
|
+
**Returns**: `Array<KBS::Blackboard::Fact>`
|
|
288
|
+
|
|
289
|
+
**Example**:
|
|
290
|
+
```ruby
|
|
291
|
+
# Query with SQL condition
|
|
292
|
+
high_temps = memory.query_facts(
|
|
293
|
+
"fact_type = ? AND json_extract(attributes, '$.value') > ?",
|
|
294
|
+
[:temperature, 80]
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Complex query
|
|
298
|
+
recent_errors = memory.query_facts(
|
|
299
|
+
"fact_type = ? AND datetime(json_extract(attributes, '$.timestamp')) > datetime(?)",
|
|
300
|
+
[:error, (Time.now - 3600).iso8601]
|
|
301
|
+
)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Important**: Only works with SQLite stores. Redis stores will raise NotImplementedError.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
### Message Queue Methods
|
|
309
|
+
|
|
310
|
+
#### `post_message(sender, topic, content, priority: 0)`
|
|
311
|
+
|
|
312
|
+
Posts a message to the blackboard message queue.
|
|
313
|
+
|
|
314
|
+
**Parameters**:
|
|
315
|
+
- `sender` (String) - Sender identifier (e.g., agent name)
|
|
316
|
+
- `topic` (String) - Message topic (channel/category)
|
|
317
|
+
- `content` (Hash) - Message payload
|
|
318
|
+
- `priority` (Integer, optional) - Message priority (default: 0, higher = more urgent)
|
|
319
|
+
|
|
320
|
+
**Returns**: `nil`
|
|
321
|
+
|
|
322
|
+
**Side Effects**:
|
|
323
|
+
- Adds message to queue
|
|
324
|
+
- Persists to store
|
|
325
|
+
|
|
326
|
+
**Example**:
|
|
327
|
+
```ruby
|
|
328
|
+
# Post high-priority alert
|
|
329
|
+
memory.post_message(
|
|
330
|
+
"temperature_agent",
|
|
331
|
+
"alerts",
|
|
332
|
+
{ level: "critical", value: 110, location: "server_room" },
|
|
333
|
+
priority: 100
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Post normal-priority task
|
|
337
|
+
memory.post_message(
|
|
338
|
+
"scheduler",
|
|
339
|
+
"tasks",
|
|
340
|
+
{ task_name: "cleanup", params: {} },
|
|
341
|
+
priority: 10
|
|
342
|
+
)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Message Ordering**: Messages consumed in priority order (highest first), then FIFO within same priority.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
#### `consume_message(topic, consumer)`
|
|
350
|
+
|
|
351
|
+
Retrieves and removes the highest priority message from a topic.
|
|
352
|
+
|
|
353
|
+
**Parameters**:
|
|
354
|
+
- `topic` (String) - Topic to consume from
|
|
355
|
+
- `consumer` (String) - Consumer identifier (for audit trail)
|
|
356
|
+
|
|
357
|
+
**Returns**: `Hash` or `nil` - Message hash with `:id`, `:sender`, `:topic`, `:content`, `:priority`, `:posted_at`, or `nil` if queue empty
|
|
358
|
+
|
|
359
|
+
**Side Effects**:
|
|
360
|
+
- Removes message from queue (atomic operation)
|
|
361
|
+
- Marks message as consumed
|
|
362
|
+
- Records consumer and consumption timestamp
|
|
363
|
+
|
|
364
|
+
**Example**:
|
|
365
|
+
```ruby
|
|
366
|
+
# Consumer loop
|
|
367
|
+
loop do
|
|
368
|
+
msg = memory.consume_message("tasks", "worker_1")
|
|
369
|
+
break unless msg
|
|
370
|
+
|
|
371
|
+
puts "Processing: #{msg[:content][:task_name]} (priority #{msg[:priority]})"
|
|
372
|
+
puts "Sent by: #{msg[:sender]} at #{msg[:posted_at]}"
|
|
373
|
+
|
|
374
|
+
# Process message...
|
|
375
|
+
process_task(msg[:content])
|
|
376
|
+
end
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Thread Safety**: Atomic pop (safe for concurrent consumers with PostgreSQL/Redis).
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
#### `peek_messages(topic, limit: 10)`
|
|
384
|
+
|
|
385
|
+
Views messages in queue without consuming them.
|
|
386
|
+
|
|
387
|
+
**Parameters**:
|
|
388
|
+
- `topic` (String) - Topic to peek
|
|
389
|
+
- `limit` (Integer, optional) - Max messages to return (default: 10)
|
|
390
|
+
|
|
391
|
+
**Returns**: `Array<Hash>` - Array of message hashes (same format as `consume_message`)
|
|
392
|
+
|
|
393
|
+
**Example**:
|
|
394
|
+
```ruby
|
|
395
|
+
# Check queue depth
|
|
396
|
+
pending = memory.peek_messages("tasks", limit: 100)
|
|
397
|
+
puts "Pending tasks: #{pending.size}"
|
|
398
|
+
|
|
399
|
+
# Inspect high-priority messages
|
|
400
|
+
pending.each do |msg|
|
|
401
|
+
if msg[:priority] > 50
|
|
402
|
+
puts "High priority: #{msg[:content][:task_name]}"
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Use Cases**:
|
|
408
|
+
- Monitor queue depth
|
|
409
|
+
- Inspect waiting messages
|
|
410
|
+
- Debugging message flow
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
### Audit Log Methods
|
|
415
|
+
|
|
416
|
+
#### `log_rule_firing(rule_name, fact_uuids, bindings = {})`
|
|
417
|
+
|
|
418
|
+
Logs a rule firing event.
|
|
419
|
+
|
|
420
|
+
**Parameters**:
|
|
421
|
+
- `rule_name` (String) - Name of fired rule
|
|
422
|
+
- `fact_uuids` (Array<String>) - UUIDs of facts that matched
|
|
423
|
+
- `bindings` (Hash, optional) - Variable bindings (default: `{}`)
|
|
424
|
+
|
|
425
|
+
**Returns**: `nil`
|
|
426
|
+
|
|
427
|
+
**Side Effects**:
|
|
428
|
+
- Adds entry to audit log
|
|
429
|
+
- Records timestamp and session ID
|
|
430
|
+
|
|
431
|
+
**Example**:
|
|
432
|
+
```ruby
|
|
433
|
+
# Typically called by engine, but can be called manually
|
|
434
|
+
memory.log_rule_firing(
|
|
435
|
+
"high_temperature_alert",
|
|
436
|
+
[fact1.uuid, fact2.uuid],
|
|
437
|
+
{ :temp? => 85, :location? => "server_room" }
|
|
438
|
+
)
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**Note**: `KBS::Blackboard::Engine` calls this automatically. Manual calls useful for custom logging.
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
#### `get_history(fact_uuid = nil, limit: 100)`
|
|
446
|
+
|
|
447
|
+
Retrieves fact change history.
|
|
448
|
+
|
|
449
|
+
**Parameters**:
|
|
450
|
+
- `fact_uuid` (String, optional) - Filter by fact UUID (default: `nil`, all facts)
|
|
451
|
+
- `limit` (Integer, optional) - Max entries to return (default: 100)
|
|
452
|
+
|
|
453
|
+
**Returns**: `Array<Hash>` - Array of history entries with `:fact_uuid`, `:fact_type`, `:attributes`, `:action`, `:timestamp`, `:session_id`
|
|
454
|
+
|
|
455
|
+
**Example**:
|
|
456
|
+
```ruby
|
|
457
|
+
# Get history for specific fact
|
|
458
|
+
fact = memory.add_fact(:temperature, value: 85)
|
|
459
|
+
memory.update_fact(fact, value: 90)
|
|
460
|
+
memory.update_fact(fact, value: 95)
|
|
461
|
+
|
|
462
|
+
history = memory.get_history(fact.uuid)
|
|
463
|
+
history.each do |entry|
|
|
464
|
+
puts "#{entry[:timestamp]}: #{entry[:action]} - #{entry[:attributes][:value]}"
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Output:
|
|
468
|
+
# 2025-01-15 10:30:03: UPDATE - 95
|
|
469
|
+
# 2025-01-15 10:30:02: UPDATE - 90
|
|
470
|
+
# 2025-01-15 10:30:00: ADD - 85
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**All Facts History**:
|
|
474
|
+
```ruby
|
|
475
|
+
# Get recent changes across all facts
|
|
476
|
+
recent_changes = memory.get_history(limit: 50)
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
#### `get_rule_firings(rule_name = nil, limit: 100)`
|
|
482
|
+
|
|
483
|
+
Retrieves rule firing history.
|
|
484
|
+
|
|
485
|
+
**Parameters**:
|
|
486
|
+
- `rule_name` (String, optional) - Filter by rule name (default: `nil`, all rules)
|
|
487
|
+
- `limit` (Integer, optional) - Max entries to return (default: 100)
|
|
488
|
+
|
|
489
|
+
**Returns**: `Array<Hash>` - Array of firing entries with `:rule_name`, `:fact_uuids`, `:bindings`, `:fired_at`, `:session_id`
|
|
490
|
+
|
|
491
|
+
**Example**:
|
|
492
|
+
```ruby
|
|
493
|
+
# Get firings for specific rule
|
|
494
|
+
firings = memory.get_rule_firings("high_temperature_alert", limit: 10)
|
|
495
|
+
firings.each do |firing|
|
|
496
|
+
puts "#{firing[:fired_at]}: #{firing[:rule_name]}"
|
|
497
|
+
puts " Bindings: #{firing[:bindings]}"
|
|
498
|
+
puts " Facts: #{firing[:fact_uuids]}"
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# All rule firings
|
|
502
|
+
all_firings = memory.get_rule_firings(limit: 100)
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
**Use Cases**:
|
|
506
|
+
- Debugging rule behavior
|
|
507
|
+
- Performance analysis
|
|
508
|
+
- Compliance auditing
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
### Knowledge Source Methods
|
|
513
|
+
|
|
514
|
+
#### `register_knowledge_source(name, description: nil, topics: [])`
|
|
515
|
+
|
|
516
|
+
Registers an agent/knowledge source.
|
|
517
|
+
|
|
518
|
+
**Parameters**:
|
|
519
|
+
- `name` (String) - Knowledge source name
|
|
520
|
+
- `description` (String, optional) - Description (default: `nil`)
|
|
521
|
+
- `topics` (Array<String>, optional) - Topics this source produces/consumes (default: `[]`)
|
|
522
|
+
|
|
523
|
+
**Returns**: `nil`
|
|
524
|
+
|
|
525
|
+
**Side Effects**:
|
|
526
|
+
- Stores knowledge source metadata in database
|
|
527
|
+
|
|
528
|
+
**Example**:
|
|
529
|
+
```ruby
|
|
530
|
+
memory.register_knowledge_source(
|
|
531
|
+
"TemperatureMonitor",
|
|
532
|
+
description: "Monitors temperature sensors and generates alerts",
|
|
533
|
+
topics: ["temperature_readings", "alerts"]
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
memory.register_knowledge_source(
|
|
537
|
+
"AlertDispatcher",
|
|
538
|
+
description: "Dispatches alerts to external systems",
|
|
539
|
+
topics: ["alerts"]
|
|
540
|
+
)
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**Use Cases**:
|
|
544
|
+
- Document multi-agent systems
|
|
545
|
+
- Visualize agent architecture
|
|
546
|
+
- Track message flow
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
### Observer Pattern Methods
|
|
551
|
+
|
|
552
|
+
#### `add_observer(observer)`
|
|
553
|
+
|
|
554
|
+
Registers an observer to receive fact change notifications.
|
|
555
|
+
|
|
556
|
+
**Parameters**:
|
|
557
|
+
- `observer` - Object responding to `update(action, fact)` method
|
|
558
|
+
|
|
559
|
+
**Returns**: `nil`
|
|
560
|
+
|
|
561
|
+
**Side Effects**: Adds observer to internal observers list
|
|
562
|
+
|
|
563
|
+
**Example**:
|
|
564
|
+
```ruby
|
|
565
|
+
class FactLogger
|
|
566
|
+
def update(action, fact)
|
|
567
|
+
case action
|
|
568
|
+
when :add
|
|
569
|
+
puts "Added: #{fact.type} #{fact.attributes}"
|
|
570
|
+
when :remove
|
|
571
|
+
puts "Removed: #{fact.uuid}"
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
logger = FactLogger.new
|
|
577
|
+
memory.add_observer(logger)
|
|
578
|
+
|
|
579
|
+
memory.add_fact(:temperature, value: 85)
|
|
580
|
+
# Output: Added: temperature {:value=>85}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
**Important**: Observers are NOT persisted. Re-register after restart.
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
### Session Management Methods
|
|
588
|
+
|
|
589
|
+
#### `clear_session`
|
|
590
|
+
|
|
591
|
+
Removes all facts from current session.
|
|
592
|
+
|
|
593
|
+
**Parameters**: None
|
|
594
|
+
|
|
595
|
+
**Returns**: `nil`
|
|
596
|
+
|
|
597
|
+
**Side Effects**:
|
|
598
|
+
- Removes facts with matching session_id
|
|
599
|
+
- Preserves audit log
|
|
600
|
+
|
|
601
|
+
**Example**:
|
|
602
|
+
```ruby
|
|
603
|
+
# Add facts
|
|
604
|
+
memory.add_fact(:temperature, value: 85)
|
|
605
|
+
memory.add_fact(:humidity, value: 60)
|
|
606
|
+
|
|
607
|
+
# Clear session facts
|
|
608
|
+
memory.clear_session
|
|
609
|
+
|
|
610
|
+
# Facts removed, but audit log intact
|
|
611
|
+
puts memory.facts.size # => 0
|
|
612
|
+
puts memory.get_history.size # => 2 (ADD entries still present)
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
#### `transaction(&block)`
|
|
618
|
+
|
|
619
|
+
Executes block within database transaction.
|
|
620
|
+
|
|
621
|
+
**Parameters**:
|
|
622
|
+
- `&block` - Block to execute
|
|
623
|
+
|
|
624
|
+
**Returns**: Result of block
|
|
625
|
+
|
|
626
|
+
**Side Effects**:
|
|
627
|
+
- Begins transaction
|
|
628
|
+
- Executes block
|
|
629
|
+
- Commits on success
|
|
630
|
+
- Rolls back on exception
|
|
631
|
+
|
|
632
|
+
**Example**:
|
|
633
|
+
```ruby
|
|
634
|
+
memory.transaction do
|
|
635
|
+
fact1 = memory.add_fact(:order, id: 1, total: 100)
|
|
636
|
+
fact2 = memory.add_fact(:inventory, item: "ABC", quantity: 10)
|
|
637
|
+
|
|
638
|
+
# If this raises, both facts are rolled back
|
|
639
|
+
raise "Validation failed" if fact1[:total] > 1000
|
|
640
|
+
end
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
**Nested Transactions**: Supported (SQLite uses savepoints).
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
### Statistics Methods
|
|
648
|
+
|
|
649
|
+
#### `stats`
|
|
650
|
+
|
|
651
|
+
Returns blackboard statistics.
|
|
652
|
+
|
|
653
|
+
**Parameters**: None
|
|
654
|
+
|
|
655
|
+
**Returns**: `Hash` with keys:
|
|
656
|
+
- `:facts_count` (Integer) - Active facts
|
|
657
|
+
- `:total_messages` (Integer) - Total messages (consumed + unconsumed)
|
|
658
|
+
- `:unconsumed_messages` (Integer) - Unconsumed messages
|
|
659
|
+
- `:rules_fired` (Integer) - Total rule firings
|
|
660
|
+
|
|
661
|
+
**Example**:
|
|
662
|
+
```ruby
|
|
663
|
+
stats = memory.stats
|
|
664
|
+
puts "Facts: #{stats[:facts_count]}"
|
|
665
|
+
puts "Messages (unconsumed): #{stats[:unconsumed_messages]}"
|
|
666
|
+
puts "Messages (total): #{stats[:total_messages]}"
|
|
667
|
+
puts "Rules fired: #{stats[:rules_fired]}"
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
### Maintenance Methods
|
|
673
|
+
|
|
674
|
+
#### `vacuum`
|
|
675
|
+
|
|
676
|
+
Optimizes database storage (SQLite only).
|
|
677
|
+
|
|
678
|
+
**Parameters**: None
|
|
679
|
+
|
|
680
|
+
**Returns**: `nil`
|
|
681
|
+
|
|
682
|
+
**Side Effects**: Reclaims unused database space
|
|
683
|
+
|
|
684
|
+
**Example**:
|
|
685
|
+
```ruby
|
|
686
|
+
# After deleting many facts
|
|
687
|
+
memory.vacuum
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
**When to Use**: After bulk deletions or periodically for long-running systems.
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
#### `close`
|
|
695
|
+
|
|
696
|
+
Closes database connection.
|
|
697
|
+
|
|
698
|
+
**Parameters**: None
|
|
699
|
+
|
|
700
|
+
**Returns**: `nil`
|
|
701
|
+
|
|
702
|
+
**Side Effects**: Closes connection to store
|
|
703
|
+
|
|
704
|
+
**Example**:
|
|
705
|
+
```ruby
|
|
706
|
+
memory = KBS::Blackboard::Memory.new(db_path: 'kb.db')
|
|
707
|
+
# ... use memory ...
|
|
708
|
+
memory.close
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
**Important**: Required for proper cleanup. Use `ensure` block:
|
|
712
|
+
```ruby
|
|
713
|
+
memory = KBS::Blackboard::Memory.new(db_path: 'kb.db')
|
|
714
|
+
begin
|
|
715
|
+
# ... use memory ...
|
|
716
|
+
ensure
|
|
717
|
+
memory.close
|
|
718
|
+
end
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
---
|
|
722
|
+
|
|
723
|
+
## KBS::Blackboard::MessageQueue
|
|
724
|
+
|
|
725
|
+
Priority-based message queue for inter-agent communication.
|
|
726
|
+
|
|
727
|
+
**Typically accessed via**: `memory.message_queue` or `memory.post_message()` / `memory.consume_message()`
|
|
728
|
+
|
|
729
|
+
### Methods
|
|
730
|
+
|
|
731
|
+
#### `post(sender, topic, content, priority: 0)`
|
|
732
|
+
|
|
733
|
+
Posts a message to the queue.
|
|
734
|
+
|
|
735
|
+
**Parameters**:
|
|
736
|
+
- `sender` (String) - Sender identifier
|
|
737
|
+
- `topic` (String) - Message topic
|
|
738
|
+
- `content` (Hash or String) - Message payload (auto-converts to JSON)
|
|
739
|
+
- `priority` (Integer, optional) - Priority (default: 0)
|
|
740
|
+
|
|
741
|
+
**Returns**: `nil`
|
|
742
|
+
|
|
743
|
+
**Example**:
|
|
744
|
+
```ruby
|
|
745
|
+
memory.message_queue.post("agent1", "alerts", { alert: "critical" }, priority: 100)
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
#### `consume(topic, consumer)`
|
|
751
|
+
|
|
752
|
+
Consumes highest priority message from topic.
|
|
753
|
+
|
|
754
|
+
**Parameters**:
|
|
755
|
+
- `topic` (String) - Topic to consume from
|
|
756
|
+
- `consumer` (String) - Consumer identifier
|
|
757
|
+
|
|
758
|
+
**Returns**: `Hash` or `nil`
|
|
759
|
+
|
|
760
|
+
**Example**:
|
|
761
|
+
```ruby
|
|
762
|
+
msg = memory.message_queue.consume("tasks", "worker_1")
|
|
763
|
+
puts msg[:content] if msg
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
#### `peek(topic, limit: 10)`
|
|
769
|
+
|
|
770
|
+
Views messages without consuming.
|
|
771
|
+
|
|
772
|
+
**Parameters**:
|
|
773
|
+
- `topic` (String) - Topic to peek
|
|
774
|
+
- `limit` (Integer, optional) - Max messages (default: 10)
|
|
775
|
+
|
|
776
|
+
**Returns**: `Array<Hash>`
|
|
777
|
+
|
|
778
|
+
**Example**:
|
|
779
|
+
```ruby
|
|
780
|
+
pending = memory.message_queue.peek("tasks", limit: 5)
|
|
781
|
+
puts "Next #{pending.size} tasks:"
|
|
782
|
+
pending.each { |m| puts " - #{m[:content]}" }
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
#### `stats`
|
|
788
|
+
|
|
789
|
+
Returns queue statistics.
|
|
790
|
+
|
|
791
|
+
**Returns**: `Hash` with `:total_messages`, `:unconsumed_messages`
|
|
792
|
+
|
|
793
|
+
**Example**:
|
|
794
|
+
```ruby
|
|
795
|
+
stats = memory.message_queue.stats
|
|
796
|
+
puts "Queue depth: #{stats[:unconsumed_messages]}"
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
---
|
|
800
|
+
|
|
801
|
+
## KBS::Blackboard::AuditLog
|
|
802
|
+
|
|
803
|
+
Complete audit trail of all fact changes and rule firings.
|
|
804
|
+
|
|
805
|
+
**Typically accessed via**: `memory.audit_log` or `memory.get_history()` / `memory.get_rule_firings()`
|
|
806
|
+
|
|
807
|
+
### Methods
|
|
808
|
+
|
|
809
|
+
#### `log_fact_change(fact_uuid, fact_type, attributes, action)`
|
|
810
|
+
|
|
811
|
+
Logs a fact change event.
|
|
812
|
+
|
|
813
|
+
**Parameters**:
|
|
814
|
+
- `fact_uuid` (String) - Fact UUID
|
|
815
|
+
- `fact_type` (Symbol) - Fact type
|
|
816
|
+
- `attributes` (Hash) - Fact attributes
|
|
817
|
+
- `action` (String) - Action: "ADD", "UPDATE", "REMOVE"
|
|
818
|
+
|
|
819
|
+
**Returns**: `nil`
|
|
820
|
+
|
|
821
|
+
**Example**:
|
|
822
|
+
```ruby
|
|
823
|
+
memory.audit_log.log_fact_change(
|
|
824
|
+
fact.uuid,
|
|
825
|
+
:temperature,
|
|
826
|
+
{ value: 85 },
|
|
827
|
+
'ADD'
|
|
828
|
+
)
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
**Note**: Automatically called by Memory. Manual calls useful for custom tracking.
|
|
832
|
+
|
|
833
|
+
---
|
|
834
|
+
|
|
835
|
+
#### `log_rule_firing(rule_name, fact_uuids, bindings = {})`
|
|
836
|
+
|
|
837
|
+
Logs a rule firing event.
|
|
838
|
+
|
|
839
|
+
**Parameters**:
|
|
840
|
+
- `rule_name` (String) - Rule name
|
|
841
|
+
- `fact_uuids` (Array<String>) - Matched fact UUIDs
|
|
842
|
+
- `bindings` (Hash, optional) - Variable bindings (default: `{}`)
|
|
843
|
+
|
|
844
|
+
**Returns**: `nil`
|
|
845
|
+
|
|
846
|
+
**Example**:
|
|
847
|
+
```ruby
|
|
848
|
+
memory.audit_log.log_rule_firing(
|
|
849
|
+
"high_temp_alert",
|
|
850
|
+
[fact1.uuid, fact2.uuid],
|
|
851
|
+
{ :temp? => 85 }
|
|
852
|
+
)
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
---
|
|
856
|
+
|
|
857
|
+
#### `fact_history(fact_uuid = nil, limit: 100)`
|
|
858
|
+
|
|
859
|
+
Retrieves fact change history.
|
|
860
|
+
|
|
861
|
+
**Parameters**:
|
|
862
|
+
- `fact_uuid` (String, optional) - Filter by UUID (default: `nil`)
|
|
863
|
+
- `limit` (Integer, optional) - Max entries (default: 100)
|
|
864
|
+
|
|
865
|
+
**Returns**: `Array<Hash>`
|
|
866
|
+
|
|
867
|
+
**Example**:
|
|
868
|
+
```ruby
|
|
869
|
+
history = memory.audit_log.fact_history(fact.uuid, limit: 10)
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
---
|
|
873
|
+
|
|
874
|
+
#### `rule_firings(rule_name = nil, limit: 100)`
|
|
875
|
+
|
|
876
|
+
Retrieves rule firing history.
|
|
877
|
+
|
|
878
|
+
**Parameters**:
|
|
879
|
+
- `rule_name` (String, optional) - Filter by rule name (default: `nil`)
|
|
880
|
+
- `limit` (Integer, optional) - Max entries (default: 100)
|
|
881
|
+
|
|
882
|
+
**Returns**: `Array<Hash>`
|
|
883
|
+
|
|
884
|
+
**Example**:
|
|
885
|
+
```ruby
|
|
886
|
+
firings = memory.audit_log.rule_firings("my_rule", limit: 50)
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
---
|
|
890
|
+
|
|
891
|
+
#### `stats`
|
|
892
|
+
|
|
893
|
+
Returns audit log statistics.
|
|
894
|
+
|
|
895
|
+
**Returns**: `Hash` with `:rules_fired`
|
|
896
|
+
|
|
897
|
+
**Example**:
|
|
898
|
+
```ruby
|
|
899
|
+
stats = memory.audit_log.stats
|
|
900
|
+
puts "Total rule firings: #{stats[:rules_fired]}"
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
---
|
|
904
|
+
|
|
905
|
+
## Usage Patterns
|
|
906
|
+
|
|
907
|
+
### 1. Multi-Agent Coordination
|
|
908
|
+
|
|
909
|
+
```ruby
|
|
910
|
+
# Setup
|
|
911
|
+
memory = KBS::Blackboard::Memory.new(db_path: 'agents.db')
|
|
912
|
+
|
|
913
|
+
# Agent 1 - Temperature Monitor
|
|
914
|
+
memory.register_knowledge_source(
|
|
915
|
+
"TempMonitor",
|
|
916
|
+
description: "Monitors temperature sensors",
|
|
917
|
+
topics: ["sensors", "alerts"]
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
def monitor_loop(memory)
|
|
921
|
+
loop do
|
|
922
|
+
temp = read_sensor
|
|
923
|
+
fact = memory.add_fact(:temperature, value: temp, timestamp: Time.now)
|
|
924
|
+
|
|
925
|
+
if temp > 80
|
|
926
|
+
memory.post_message(
|
|
927
|
+
"TempMonitor",
|
|
928
|
+
"alerts",
|
|
929
|
+
{ type: "high_temp", value: temp },
|
|
930
|
+
priority: 50
|
|
931
|
+
)
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
sleep 5
|
|
935
|
+
end
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
# Agent 2 - Alert Dispatcher
|
|
939
|
+
memory.register_knowledge_source(
|
|
940
|
+
"AlertDispatcher",
|
|
941
|
+
description: "Sends alerts to external systems",
|
|
942
|
+
topics: ["alerts"]
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
def dispatch_loop(memory)
|
|
946
|
+
loop do
|
|
947
|
+
msg = memory.consume_message("alerts", "AlertDispatcher")
|
|
948
|
+
break unless msg
|
|
949
|
+
|
|
950
|
+
case msg[:content][:type]
|
|
951
|
+
when "high_temp"
|
|
952
|
+
send_email_alert(msg[:content][:value])
|
|
953
|
+
end
|
|
954
|
+
end
|
|
955
|
+
end
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
---
|
|
959
|
+
|
|
960
|
+
### 2. Audit Trail Analysis
|
|
961
|
+
|
|
962
|
+
```ruby
|
|
963
|
+
# Find facts that were updated multiple times
|
|
964
|
+
memory.get_history(limit: 1000).group_by { |e| e[:fact_uuid] }.each do |uuid, entries|
|
|
965
|
+
if entries.size > 5
|
|
966
|
+
puts "Fact #{uuid} changed #{entries.size} times"
|
|
967
|
+
entries.each do |entry|
|
|
968
|
+
puts " #{entry[:timestamp]}: #{entry[:action]} - #{entry[:attributes]}"
|
|
969
|
+
end
|
|
970
|
+
end
|
|
971
|
+
end
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
---
|
|
975
|
+
|
|
976
|
+
### 3. Rule Performance Analysis
|
|
977
|
+
|
|
978
|
+
```ruby
|
|
979
|
+
# Analyze rule firing frequency
|
|
980
|
+
firings = memory.get_rule_firings(limit: 10000)
|
|
981
|
+
by_rule = firings.group_by { |f| f[:rule_name] }
|
|
982
|
+
|
|
983
|
+
by_rule.each do |rule_name, firings_list|
|
|
984
|
+
puts "#{rule_name}: #{firings_list.size} firings"
|
|
985
|
+
|
|
986
|
+
# Calculate average time between firings
|
|
987
|
+
if firings_list.size > 1
|
|
988
|
+
times = firings_list.map { |f| f[:fired_at] }.sort
|
|
989
|
+
intervals = times.each_cons(2).map { |t1, t2| (t2 - t1).to_f }
|
|
990
|
+
avg_interval = intervals.sum / intervals.size
|
|
991
|
+
puts " Avg interval: #{avg_interval.round(2)} seconds"
|
|
992
|
+
end
|
|
993
|
+
end
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
---
|
|
997
|
+
|
|
998
|
+
### 4. Transaction-Based Workflows
|
|
999
|
+
|
|
1000
|
+
```ruby
|
|
1001
|
+
def process_order(memory, order_data)
|
|
1002
|
+
memory.transaction do
|
|
1003
|
+
# Add order fact
|
|
1004
|
+
order = memory.add_fact(:order, order_data)
|
|
1005
|
+
|
|
1006
|
+
# Check inventory
|
|
1007
|
+
inventory = memory.get_facts(:inventory, product_id: order[:product_id]).first
|
|
1008
|
+
raise "Insufficient inventory" if inventory[:quantity] < order[:quantity]
|
|
1009
|
+
|
|
1010
|
+
# Deduct inventory
|
|
1011
|
+
memory.update_fact(inventory, quantity: inventory[:quantity] - order[:quantity])
|
|
1012
|
+
|
|
1013
|
+
# Create shipment fact
|
|
1014
|
+
shipment = memory.add_fact(:shipment, order_id: order.uuid, status: "pending")
|
|
1015
|
+
|
|
1016
|
+
# Post message for shipping agent
|
|
1017
|
+
memory.post_message(
|
|
1018
|
+
"OrderProcessor",
|
|
1019
|
+
"shipments",
|
|
1020
|
+
{ shipment_id: shipment.uuid },
|
|
1021
|
+
priority: 10
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
# If any step fails, entire transaction rolls back
|
|
1025
|
+
end
|
|
1026
|
+
end
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
---
|
|
1030
|
+
|
|
1031
|
+
### 5. Debugging Message Flow
|
|
1032
|
+
|
|
1033
|
+
```ruby
|
|
1034
|
+
# Monitor message queue
|
|
1035
|
+
def monitor_queue(memory, topic)
|
|
1036
|
+
loop do
|
|
1037
|
+
pending = memory.peek_messages(topic, limit: 10)
|
|
1038
|
+
puts "#{Time.now}: #{pending.size} messages in #{topic} queue"
|
|
1039
|
+
|
|
1040
|
+
pending.each do |msg|
|
|
1041
|
+
age = Time.now - msg[:posted_at]
|
|
1042
|
+
puts " [#{msg[:priority]}] #{msg[:sender]}: #{msg[:content]} (#{age.round}s old)"
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
sleep 5
|
|
1046
|
+
end
|
|
1047
|
+
end
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
---
|
|
1051
|
+
|
|
1052
|
+
### 6. Session Isolation
|
|
1053
|
+
|
|
1054
|
+
```ruby
|
|
1055
|
+
# Separate test runs
|
|
1056
|
+
test_memory = KBS::Blackboard::Memory.new(db_path: 'test.db')
|
|
1057
|
+
puts "Session: #{test_memory.session_id}"
|
|
1058
|
+
|
|
1059
|
+
# Run test
|
|
1060
|
+
test_memory.add_fact(:test_marker, run_id: 1)
|
|
1061
|
+
run_tests(test_memory)
|
|
1062
|
+
|
|
1063
|
+
# Cleanup session (preserves audit log)
|
|
1064
|
+
test_memory.clear_session
|
|
1065
|
+
|
|
1066
|
+
# Analyze audit log across sessions
|
|
1067
|
+
all_history = test_memory.get_history(limit: 10000)
|
|
1068
|
+
by_session = all_history.group_by { |e| e[:session_id] }
|
|
1069
|
+
puts "Total sessions: #{by_session.size}"
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
---
|
|
1073
|
+
|
|
1074
|
+
### 7. Custom Observer for Metrics
|
|
1075
|
+
|
|
1076
|
+
```ruby
|
|
1077
|
+
class MetricsObserver
|
|
1078
|
+
def initialize
|
|
1079
|
+
@fact_counts = Hash.new(0)
|
|
1080
|
+
@add_count = 0
|
|
1081
|
+
@remove_count = 0
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
def update(action, fact)
|
|
1085
|
+
case action
|
|
1086
|
+
when :add
|
|
1087
|
+
@add_count += 1
|
|
1088
|
+
@fact_counts[fact.type] += 1
|
|
1089
|
+
when :remove
|
|
1090
|
+
@remove_count += 1
|
|
1091
|
+
@fact_counts[fact.type] -= 1
|
|
1092
|
+
end
|
|
1093
|
+
end
|
|
1094
|
+
|
|
1095
|
+
def report
|
|
1096
|
+
puts "Facts added: #{@add_count}"
|
|
1097
|
+
puts "Facts removed: #{@remove_count}"
|
|
1098
|
+
puts "Active facts by type:"
|
|
1099
|
+
@fact_counts.each do |type, count|
|
|
1100
|
+
puts " #{type}: #{count}"
|
|
1101
|
+
end
|
|
1102
|
+
end
|
|
1103
|
+
end
|
|
1104
|
+
|
|
1105
|
+
metrics = MetricsObserver.new
|
|
1106
|
+
memory.add_observer(metrics)
|
|
1107
|
+
|
|
1108
|
+
# ... run system ...
|
|
1109
|
+
|
|
1110
|
+
metrics.report
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
---
|
|
1114
|
+
|
|
1115
|
+
## Performance Considerations
|
|
1116
|
+
|
|
1117
|
+
### Message Queue
|
|
1118
|
+
|
|
1119
|
+
- **Priority indexing**: Messages sorted by priority + timestamp
|
|
1120
|
+
- **Atomic pop**: `consume` uses SELECT + UPDATE in transaction (safe for concurrent consumers)
|
|
1121
|
+
- **Scaling**: For >10,000 messages/sec, use Redis store
|
|
1122
|
+
|
|
1123
|
+
### Audit Log
|
|
1124
|
+
|
|
1125
|
+
- **Write performance**: Each fact change = 1 audit log insert (can be disabled for high-throughput)
|
|
1126
|
+
- **Query performance**: Indexed by `fact_uuid` and `session_id`
|
|
1127
|
+
- **Growth**: Audit log grows unbounded. Implement periodic archival for production:
|
|
1128
|
+
|
|
1129
|
+
```ruby
|
|
1130
|
+
# Archive old audit entries
|
|
1131
|
+
def archive_old_audit(memory, cutoff_date)
|
|
1132
|
+
memory.store.db.execute(
|
|
1133
|
+
"DELETE FROM fact_history WHERE timestamp < ?",
|
|
1134
|
+
[cutoff_date.iso8601]
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
memory.store.db.execute(
|
|
1138
|
+
"DELETE FROM rules_fired WHERE fired_at < ?",
|
|
1139
|
+
[cutoff_date.iso8601]
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
memory.vacuum
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
# Archive entries older than 30 days
|
|
1146
|
+
archive_old_audit(memory, Date.today - 30)
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
---
|
|
1150
|
+
|
|
1151
|
+
## See Also
|
|
1152
|
+
|
|
1153
|
+
- [Engine API](engine.md) - Blackboard::Engine integration
|
|
1154
|
+
- [Facts API](facts.md) - Persistent fact objects
|
|
1155
|
+
- [Custom Persistence](../advanced/custom-persistence.md) - Implementing custom stores
|
|
1156
|
+
- [Blackboard Guide](../guides/blackboard-memory.md) - Blackboard pattern overview
|
|
1157
|
+
- [Multi-Agent Example](../examples/multi-agent.md) - Multi-agent coordination
|